From 31d36e7abf89f188fffba004520e4cbe2c4890a0 Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Tue, 27 Aug 2024 08:46:15 +0300 Subject: [PATCH 001/701] Bugfix: BitstreamRestController etag/content-length calculation does not consider cover page Ported Alphonse Bendt's PR #9666 to DSpace 7 branch (squashed 5 commits). This PR fixes a bug where the etag/content-length calculation did not respect the potential existence of a coverpage. The controller now will use the post processed pdf if coverpages are enabled. --- .../app/rest/BitstreamRestController.java | 14 +- .../app/rest/utils/BitstreamResource.java | 119 ++++++++++------ .../app/rest/BitstreamRestControllerIT.java | 129 +++++++++++++++++- 3 files changed, 209 insertions(+), 53 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java index 22b18724b90b..242b231654c6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java @@ -135,11 +135,16 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp long filesize = bit.getSizeBytes(); Boolean citationEnabledForBitstream = citationDocumentService.isCitationEnabledForBitstream(bit, context); + var bitstreamResource = + new org.dspace.app.rest.utils.BitstreamResource(name, uuid, + currentUser != null ? currentUser.getID() : null, + context.getSpecialGroupUuids(), citationEnabledForBitstream); + HttpHeadersInitializer httpHeadersInitializer = new HttpHeadersInitializer() .withBufferSize(BUFFER_SIZE) .withFileName(name) - .withChecksum(bit.getChecksum()) - .withLength(bit.getSizeBytes()) + .withChecksum(bitstreamResource.getChecksum()) + .withLength(bitstreamResource.contentLength()) .withMimetype(mimetype) .with(request) .with(response); @@ -157,11 +162,6 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT); } - org.dspace.app.rest.utils.BitstreamResource bitstreamResource = - new org.dspace.app.rest.utils.BitstreamResource(name, uuid, - currentUser != null ? currentUser.getID() : null, - context.getSpecialGroupUuids(), citationEnabledForBitstream); - //We have all the data we need, close the connection to the database so that it doesn't stay open during //download/streaming context.complete(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java index 4e5545fabc7f..2f47dacc7ccc 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java @@ -15,7 +15,8 @@ import java.util.UUID; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.tuple.Pair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.factory.ContentServiceFactory; @@ -27,6 +28,7 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.utils.DSpace; import org.springframework.core.io.AbstractResource; +import org.springframework.util.DigestUtils; /** * This class acts as a {@link AbstractResource} used by Spring's framework to send the data in a proper and @@ -36,18 +38,23 @@ */ public class BitstreamResource extends AbstractResource { - private String name; - private UUID uuid; - private UUID currentUserUUID; - private boolean shouldGenerateCoverPage; - private byte[] file; - private Set currentSpecialGroups; + private static final Logger LOG = LogManager.getLogger(BitstreamResource.class); - private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); - private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); - private CitationDocumentService citationDocumentService = + private final String name; + private final UUID uuid; + private final UUID currentUserUUID; + private final boolean shouldGenerateCoverPage; + private final Set currentSpecialGroups; + + private final BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + private final EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + private final CitationDocumentService citationDocumentService = new DSpace().getServiceManager() - .getServicesByType(CitationDocumentService.class).get(0); + .getServicesByType(CitationDocumentService.class).get(0); + + private String documentEtag; + private long documentLength; + private InputStream documentInputStream = null; public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, Set currentSpecialGroups, boolean shouldGenerateCoverPage) { @@ -68,16 +75,14 @@ public BitstreamResource(String name, UUID uuid, UUID currentUserUUID, Set */ private byte[] getCoverpageByteArray(Context context, Bitstream bitstream) throws IOException, SQLException, AuthorizeException { - if (file == null) { - try { - Pair citedDocument = citationDocumentService.makeCitedDocument(context, bitstream); - this.file = citedDocument.getLeft(); - } catch (Exception e) { - // Return the original bitstream without the cover page - this.file = IOUtils.toByteArray(bitstreamService.retrieve(context, bitstream)); - } + try { + var citedDocument = citationDocumentService.makeCitedDocument(context, bitstream); + return citedDocument.getLeft(); + } catch (Exception e) { + LOG.warn("Could not generate cover page. Will fallback to original document", e); + // Return the original bitstream without the cover page + return IOUtils.toByteArray(bitstreamService.retrieve(context, bitstream)); } - return file; } @Override @@ -87,22 +92,9 @@ public String getDescription() { @Override public InputStream getInputStream() throws IOException { - try (Context context = initializeContext()) { - - Bitstream bitstream = bitstreamService.find(context, uuid); - InputStream out; - - if (shouldGenerateCoverPage) { - out = new ByteArrayInputStream(getCoverpageByteArray(context, bitstream)); - } else { - out = bitstreamService.retrieve(context, bitstream); - } + fetchDocument(); - this.file = null; - return out; - } catch (SQLException | AuthorizeException e) { - throw new IOException(e); - } + return this.documentInputStream; } @Override @@ -111,17 +103,63 @@ public String getFilename() { } @Override - public long contentLength() throws IOException { + public long contentLength() { + fetchDocument(); + + return this.documentLength; + } + + public String getChecksum() { + fetchDocument(); + + return this.documentEtag; + } + + private void fetchDocument() { + if (this.documentInputStream != null) { + return; + } + try (Context context = initializeContext()) { Bitstream bitstream = bitstreamService.find(context, uuid); if (shouldGenerateCoverPage) { - return getCoverpageByteArray(context, bitstream).length; + var coverPage = getCoverpageByteArray(context, bitstream); + + this.documentEtag = etag(bitstream); + this.documentLength = coverPage.length; + this.documentInputStream = new ByteArrayInputStream(coverPage); + } else { - return bitstream.getSizeBytes(); + + this.documentEtag = bitstream.getChecksum(); + this.documentLength = bitstream.getSizeBytes(); + this.documentInputStream = bitstreamService.retrieve(context, bitstream); + } - } catch (SQLException | AuthorizeException e) { - throw new IOException(e); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(e); } + + LOG.debug("fetched document {} {} {}", shouldGenerateCoverPage, this.documentEtag, this.documentLength); + } + + private String etag(Bitstream bitstream) { + + /* Ideally we would calculate the md5 checksum based on the document with coverpage. + However it looks like the coverpage generation is not stable (e.g. if invoked twice it will return + different results). This means we cannot use it for etag calculation/comparison! + + Instead we will create the MD5 based off the original checksum plus fixed prefix. This ensures + that checksums will differ when coverpage is on/off. + However the checksum will _not_ change if the coverpage content changes. + */ + + var content = "coverpage:" + bitstream.getChecksum(); + + StringBuilder builder = new StringBuilder(37); + DigestUtils.appendMd5DigestAsHex(content.getBytes(), builder); + + return builder.toString(); } private Context initializeContext() throws SQLException { @@ -131,4 +169,5 @@ private Context initializeContext() throws SQLException { currentSpecialGroups.forEach(context::setSpecialGroup); return context; } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 855fedbaa2ab..45f8565fbaf3 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -23,6 +23,7 @@ import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; @@ -49,8 +50,10 @@ import java.io.InputStream; import java.io.StringWriter; import java.io.Writer; +import java.nio.file.Files; import java.util.UUID; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; @@ -90,6 +93,7 @@ import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; /** * Integration test to test the /api/core/bitstreams/[id]/* endpoints @@ -948,12 +952,11 @@ public void retrieveCitationCoverpageOfBitstream() throws Exception { //** THEN ** .andExpect(status().isOk()) - //The Content Length must match the full length + // exact content-length and etag values are verified in s separate test .andExpect(header().string("Content-Length", not(nullValue()))) + .andExpect(header().string("ETag", not(nullValue()))) //The server should indicate we support Range requests .andExpect(header().string("Accept-Ranges", "bytes")) - //The ETag has to be based on the checksum - .andExpect(header().string("ETag", "\"" + bitstream.getChecksum() + "\"")) //We expect the content type to match the bitstream mime type .andExpect(content().contentType("application/pdf;charset=UTF-8")) //THe bytes of the content must match the original content @@ -962,20 +965,22 @@ public void retrieveCitationCoverpageOfBitstream() throws Exception { // The citation cover page contains the item title. // We will now verify that the pdf text contains this title. String pdfText = extractPDFText(content); - System.out.println(pdfText); assertTrue(StringUtils.contains(pdfText,"Public item citation cover page test 1")); // The dspace-api/src/test/data/dspaceFolder/assetstore/ConstitutionofIreland.pdf file contains 64 pages, // manually counted + 1 citation cover page assertEquals(65,getNumberOfPdfPages(content)); + var etagHeader = getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andReturn().getResponse().getHeader("ETag"); + //A If-None-Match HEAD request on the ETag must tell is the bitstream is not modified getClient().perform(head("/api/core/bitstreams/" + bitstream.getID() + "/content") - .header("If-None-Match", bitstream.getChecksum())) + .header("If-None-Match", etagHeader)) .andExpect(status().isNotModified()); //The download and head request should also be logged as a statistics record - checkNumberOfStatsRecords(bitstream, 2); + checkNumberOfStatsRecords(bitstream, 3); } private String extractPDFText(byte[] content) throws IOException { @@ -1383,4 +1388,116 @@ private void verifyBitstreamDownload(Bitstream file, String contentType, boolean header.contains("attachment")); } } + + @Test + public void contentLengthAndEtagUsesOriginalBitstream() throws Exception { + givenPdf(false, originalPdf -> { + var originalMd5 = md5Checksum(originalPdf); + long originalLength = Files.size(originalPdf.toPath()); + + assertThat(originalLength, greaterThan(0L)); + + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(header().longValue("Content-Length", originalLength)) + .andExpect(header().string("ETag", "\"" + originalMd5 + "\"")); + }); + } + + private static String md5Checksum(File file) throws IOException { + final String md5; + try (InputStream is = new FileInputStream(file)) { + md5 = DigestUtils.md5Hex(is); + } + return md5; + } + + @Test + public void withCoverPageContentLengthAndEtagChanges() throws Exception { + givenPdf(true, originalPdf -> { + var originalMd5 = md5Checksum(originalPdf); + long originalLength = Files.size(originalPdf.toPath()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Length", not(Long.toString(originalLength)))) + .andExpect(header().string("ETag", not("\"" + originalMd5 + "\""))); + }); + } + + @Test + public void etagAndContentLengthIsStable() { + givenPdf(false, ignored -> { + var etag1 = getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andReturn().getResponse().getHeader("Etag"); + + var etag2 = getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andReturn().getResponse().getHeader("Etag"); + + assertThat(etag1, equalTo(etag2)); + }); + } + + @Test + public void withCoverPageEtagAndContentLengthIsStable() { + givenPdf(true, ignored -> { + var etag1 = getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andReturn().getResponse().getHeader("Etag"); + + var etag2 = getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andReturn().getResponse().getHeader("Etag"); + + assertThat(etag1, equalTo(etag2)); + }); + } + + @FunctionalInterface + interface ThrowingConsumer { + void accept(T t) throws Exception; + } + + private void givenPdf(boolean coverPageEnabled, ThrowingConsumer block) { + configurationService.setProperty("citation-page.enable_globally", coverPageEnabled); + + try { + citationDocumentService.afterPropertiesSet(); + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community and one collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = + CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + //2. A public item with a bitstream + File originalPdf = new File(testProps.getProperty("test.bitstream")); + + try (InputStream is = new FileInputStream(originalPdf)) { + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item citation cover page test 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .build(); + + bitstream = BitstreamBuilder + .createBitstream(context, publicItem1, is) + .withName("Test bitstream") + .withDescription("This is a bitstream to test the citation cover page.") + .withMimeType("application/pdf") + .build(); + } + context.restoreAuthSystemState(); + + block.accept(originalPdf); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } From 745e9c468a083b916f89682f414126e0f044bc7c Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 16 Jan 2025 12:01:13 +0100 Subject: [PATCH 002/701] 124362: Fix issue with the VersionedHandleIdentifierProviderWithCanonicalHandles and creating communities / collections --- ...dentifierProviderWithCanonicalHandles.java | 38 ++++++++++++------- .../config/spring/api/identifier-service.xml | 9 ++--- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java b/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java index 78ad6b7b79bb..7be5cbc0d8a8 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java +++ b/dspace-api/src/main/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandles.java @@ -15,11 +15,14 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; +import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.MetadataValue; -import org.dspace.content.service.ItemService; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogHelper; @@ -64,9 +67,6 @@ public class VersionedHandleIdentifierProviderWithCanonicalHandles extends Ident @Autowired(required = true) private HandleService handleService; - @Autowired(required = true) - private ItemService itemService; - /** * After all the properties are set check that the versioning is enabled * @@ -173,6 +173,16 @@ public String register(Context context, DSpaceObject dso) { throw new RuntimeException("The current user is not authorized to change this item.", ex); } } + if (dso instanceof Collection || dso instanceof Community) { + try { + // Update the metadata with the handle for collections and communities. + modifyHandleMetadata(context, dso, getCanonical(id)); + } catch (SQLException ex) { + throw new RuntimeException("A problem with the database connection occured.", ex); + } catch (AuthorizeException ex) { + throw new RuntimeException("The current user is not authorized to change this item.", ex); + } + } return id; } @@ -490,27 +500,29 @@ protected String getCanonical(String identifier) { * Remove all handles from an item's metadata and add the supplied handle instead. * * @param context The relevant DSpace Context. - * @param item which item to modify + * @param dso which dso to modify * @param handle which handle to add * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - protected void modifyHandleMetadata(Context context, Item item, String handle) + protected void modifyHandleMetadata(Context context, DSpaceObject dso, String handle) throws SQLException, AuthorizeException { // we want to exchange the old handle against the new one. To do so, we // load all identifiers, clear the metadata field, re add all // identifiers which are not from type handle and add the new handle. String handleref = handleService.getCanonicalForm(handle); - List identifiers = itemService - .getMetadata(item, MetadataSchemaEnum.DC.getName(), "identifier", "uri", Item.ANY); - itemService.clearMetadata(context, item, MetadataSchemaEnum.DC.getName(), "identifier", "uri", Item.ANY); + DSpaceObjectService dSpaceObjectService = + ContentServiceFactory.getInstance().getDSpaceObjectService(dso); + List identifiers = dSpaceObjectService + .getMetadata(dso, MetadataSchemaEnum.DC.getName(), "identifier", "uri", Item.ANY); + dSpaceObjectService.clearMetadata(context, dso, MetadataSchemaEnum.DC.getName(), "identifier", "uri", Item.ANY); for (MetadataValue identifier : identifiers) { if (this.supports(identifier.getValue())) { // ignore handles continue; } - itemService.addMetadata(context, - item, + dSpaceObjectService.addMetadata(context, + dso, identifier.getMetadataField(), identifier.getLanguage(), identifier.getValue(), @@ -518,9 +530,9 @@ protected void modifyHandleMetadata(Context context, Item item, String handle) identifier.getConfidence()); } if (!StringUtils.isEmpty(handleref)) { - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), + dSpaceObjectService.addMetadata(context, dso, MetadataSchemaEnum.DC.getName(), "identifier", "uri", null, handleref); } - itemService.update(context, item); + dSpaceObjectService.update(context, dso); } } diff --git a/dspace/config/spring/api/identifier-service.xml b/dspace/config/spring/api/identifier-service.xml index 0c58cc1de932..9e599527444a 100644 --- a/dspace/config/spring/api/identifier-service.xml +++ b/dspace/config/spring/api/identifier-service.xml @@ -17,9 +17,9 @@ The VersionedHandleIdentifierProvider creates a new versioned handle for every new version. --> - - - + + + - - - - + + + + - + @@ -1948,7 +1948,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git git@github.com:DSpace/DSpace.git - dspace-7.6.3 + dspace-7_x From a2cb8cc8383aa64beb51509a730fcaea0965df37 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 10 Feb 2025 10:14:36 -0600 Subject: [PATCH 009/701] Remove unused byte-buddy --- pom.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pom.xml b/pom.xml index 9695ae68300c..28be939a9923 100644 --- a/pom.xml +++ b/pom.xml @@ -1839,13 +1839,6 @@ javax.annotation-api ${javax-annotation.version} - - - - net.bytebuddy - byte-buddy - 1.16.1 - From 8e411ac70cab3d30de693b929fc4c54d46022d61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:25:47 +0000 Subject: [PATCH 010/701] Bump com.github.spotbugs:spotbugs in the build-tools group Bumps the build-tools group with 1 update: [com.github.spotbugs:spotbugs](https://github.com/spotbugs/spotbugs). Updates `com.github.spotbugs:spotbugs` from 4.9.0 to 4.9.1 - [Release notes](https://github.com/spotbugs/spotbugs/releases) - [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotbugs/spotbugs/compare/4.9.0...4.9.1) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7710ec7eed46..6c9376118057 100644 --- a/pom.xml +++ b/pom.xml @@ -305,7 +305,7 @@ com.github.spotbugs spotbugs - 4.9.0 + 4.9.1 From 06a5458205dd13de9580974cdf9cf76e823edfc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:25:53 +0000 Subject: [PATCH 011/701] Bump the google-apis group across 1 directory with 3 updates Bumps the google-apis group with 3 updates in the / directory: [com.google.http-client:google-http-client](https://github.com/googleapis/google-http-java-client), [com.google.http-client:google-http-client-jackson2](https://github.com/googleapis/google-http-java-client) and [com.google.http-client:google-http-client-gson](https://github.com/googleapis/google-http-java-client). Updates `com.google.http-client:google-http-client` from 1.45.3 to 1.46.1 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.45.3...v1.46.1) Updates `com.google.http-client:google-http-client-jackson2` from 1.45.3 to 1.46.1 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.45.3...v1.46.1) Updates `com.google.http-client:google-http-client-gson` from 1.43.3 to 1.46.1 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.43.3...v1.46.1) --- updated-dependencies: - dependency-name: com.google.http-client:google-http-client dependency-type: direct:production update-type: version-update:semver-minor dependency-group: google-apis - dependency-name: com.google.http-client:google-http-client-jackson2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: google-apis - dependency-name: com.google.http-client:google-http-client-gson dependency-type: direct:production update-type: version-update:semver-minor dependency-group: google-apis ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 7710ec7eed46..da8f18e3b96f 100644 --- a/pom.xml +++ b/pom.xml @@ -1708,7 +1708,7 @@ com.google.http-client google-http-client - 1.45.3 + 1.46.1 com.google.errorprone @@ -1730,7 +1730,7 @@ com.google.http-client google-http-client-jackson2 - 1.45.3 + 1.46.1 jackson-core @@ -1752,7 +1752,7 @@ com.google.http-client google-http-client-gson - 1.43.3 + 1.46.1 From ba318d971049ef01b1ec4a7e5daaeb06f4d53967 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:26:17 +0000 Subject: [PATCH 012/701] Bump the test-tools group with 6 updates Bumps the test-tools group with 6 updates: | Package | From | To | | --- | --- | --- | | [io.netty:netty-buffer](https://github.com/netty/netty) | `4.1.117.Final` | `4.1.118.Final` | | [io.netty:netty-transport](https://github.com/netty/netty) | `4.1.117.Final` | `4.1.118.Final` | | [io.netty:netty-transport-native-unix-common](https://github.com/netty/netty) | `4.1.117.Final` | `4.1.118.Final` | | [io.netty:netty-common](https://github.com/netty/netty) | `4.1.117.Final` | `4.1.118.Final` | | [io.netty:netty-handler](https://github.com/netty/netty) | `4.1.117.Final` | `4.1.118.Final` | | [io.netty:netty-codec](https://github.com/netty/netty) | `4.1.117.Final` | `4.1.118.Final` | Updates `io.netty:netty-buffer` from 4.1.117.Final to 4.1.118.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.117.Final...netty-4.1.118.Final) Updates `io.netty:netty-transport` from 4.1.117.Final to 4.1.118.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.117.Final...netty-4.1.118.Final) Updates `io.netty:netty-transport-native-unix-common` from 4.1.117.Final to 4.1.118.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.117.Final...netty-4.1.118.Final) Updates `io.netty:netty-common` from 4.1.117.Final to 4.1.118.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.117.Final...netty-4.1.118.Final) Updates `io.netty:netty-handler` from 4.1.117.Final to 4.1.118.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.117.Final...netty-4.1.118.Final) Updates `io.netty:netty-codec` from 4.1.117.Final to 4.1.118.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.117.Final...netty-4.1.118.Final) --- updated-dependencies: - dependency-name: io.netty:netty-buffer dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport-native-unix-common dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-common dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-handler dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-codec dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index e90773d1def4..29b9865a5a7c 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -887,32 +887,32 @@ io.netty netty-buffer - 4.1.117.Final + 4.1.118.Final io.netty netty-transport - 4.1.117.Final + 4.1.118.Final io.netty netty-transport-native-unix-common - 4.1.117.Final + 4.1.118.Final io.netty netty-common - 4.1.117.Final + 4.1.118.Final io.netty netty-handler - 4.1.117.Final + 4.1.118.Final io.netty netty-codec - 4.1.117.Final + 4.1.118.Final org.apache.velocity From 090001b685c6595450210e5fa311f928afe2c1be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:26:31 +0000 Subject: [PATCH 013/701] Bump commons-logging:commons-logging in the apache-commons group Bumps the apache-commons group with 1 update: commons-logging:commons-logging. Updates `commons-logging:commons-logging` from 1.3.4 to 1.3.5 --- updated-dependencies: - dependency-name: commons-logging:commons-logging dependency-type: direct:production update-type: version-update:semver-patch dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7710ec7eed46..505395fafec7 100644 --- a/pom.xml +++ b/pom.xml @@ -1511,7 +1511,7 @@ commons-logging commons-logging - 1.3.4 + 1.3.5 org.apache.commons From 2c13ee40fef393dabf56d35b93e7e537608af9f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 22:27:33 +0000 Subject: [PATCH 014/701] Bump tika.version from 2.9.2 to 2.9.3 Bumps `tika.version` from 2.9.2 to 2.9.3. Updates `org.apache.tika:tika-core` from 2.9.2 to 2.9.3 - [Changelog](https://github.com/apache/tika/blob/2.9.3/CHANGES.txt) - [Commits](https://github.com/apache/tika/compare/2.9.2...2.9.3) Updates `org.apache.tika:tika-parsers-standard-package` from 2.9.2 to 2.9.3 --- updated-dependencies: - dependency-name: org.apache.tika:tika-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.tika:tika-parsers-standard-package dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7710ec7eed46..ce859ee7e78f 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ 2.0.33 1.19.0 1.7.36 - 2.9.2 + 2.9.3 1.80 From 102c347455329181205a2be040def3f30e74b8ca Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 11 Feb 2025 10:39:16 -0600 Subject: [PATCH 015/701] Dependency convergence fix --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index ce859ee7e78f..313a6bbfe9af 100644 --- a/pom.xml +++ b/pom.xml @@ -1316,6 +1316,12 @@ bcutil-jdk18on ${bouncycastle.version} + + + com.healthmarketscience.jackcess + jackcess + 4.0.8 + org.apache.james From ee762260cf423075ceb6129b00cb8cb981f725ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Wed, 12 Feb 2025 12:26:27 +0000 Subject: [PATCH 016/701] [Port dspace-7_x] Fixing Crossref document type issue with new metadata mapping processor (#9909) * new metadata mapping processor for crossref document type * licence and code style fixes * adjust crossref test to consider mapped dc.type to Article * correcting english * remove trailing space --- ...nValueMappingMetadataProcessorService.java | 85 +++++++++++++++++++ ...CrossRefImportMetadataSourceServiceIT.java | 6 +- ...sref-to-dspace-publication-type.properties | 29 +++++++ .../spring/api/crossref-integration.xml | 10 +++ 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/transform/StringJsonValueMappingMetadataProcessorService.java create mode 100644 dspace/config/crosswalks/mapConverter-crossref-to-dspace-publication-type.properties diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/transform/StringJsonValueMappingMetadataProcessorService.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/transform/StringJsonValueMappingMetadataProcessorService.java new file mode 100644 index 000000000000..a8f667eb9792 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/transform/StringJsonValueMappingMetadataProcessorService.java @@ -0,0 +1,85 @@ +/** + * 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.importer.external.metadatamapping.transform; + +import static java.util.Optional.ofNullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.importer.external.metadatamapping.contributor.JsonPathMetadataProcessor; +import org.dspace.util.SimpleMapConverter; + +/** + * This class is a Metadata processor from a structured JSON Metadata result + * and uses a SimpleMapConverter, with a mapping properties file + * to map to a single string value based on mapped keys.
+ * Like:
+ * journal-article = Article + * + * @author paulo-graca + */ +public class StringJsonValueMappingMetadataProcessorService implements JsonPathMetadataProcessor { + + private final static Logger log = LogManager.getLogger(); + /** + * The value map converter. + * a list of values to map from + */ + private SimpleMapConverter valueMapConverter; + private String path; + + @Override + public Collection processMetadata(String json) { + JsonNode rootNode = convertStringJsonToJsonNode(json); + Optional abstractNode = Optional.of(rootNode.at(path)); + Collection values = new ArrayList<>(); + + if (abstractNode.isPresent() && abstractNode.get().getNodeType().equals(JsonNodeType.STRING)) { + + String stringValue = abstractNode.get().asText(); + values.add(ofNullable(stringValue) + .map(value -> valueMapConverter != null ? valueMapConverter.getValue(value) : value) + .orElse(valueMapConverter.getValue(null))); + } + return values; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = null; + try { + body = mapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process json response.", e); + } + return body; + } + + /* Getters and Setters */ + + public String convertType(String type) { + return valueMapConverter != null ? valueMapConverter.getValue(type) : type; + } + + public void setValueMapConverter(SimpleMapConverter valueMapConverter) { + this.valueMapConverter = valueMapConverter; + } + + public void setPath(String path) { + this.path = path; + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java index f61a81140ddc..a182e7d89070 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java @@ -163,7 +163,8 @@ private ArrayList getRecords() { "State of Awareness of Freshers’ Groups Chortkiv State" + " Medical College of Prevention of Iodine Deficiency Diseases"); MetadatumDTO author = createMetadatumDTO("dc", "contributor", "author", "Senyuk, L.V."); - MetadatumDTO type = createMetadatumDTO("dc", "type", null, "journal-article"); + // is expected the dc.type to be mapped from journal-article to Article + MetadatumDTO type = createMetadatumDTO("dc", "type", null, "Article"); MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2016-05-19"); MetadatumDTO ispartof = createMetadatumDTO("dc", "relation", "ispartof", "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu"); @@ -192,7 +193,8 @@ private ArrayList getRecords() { MetadatumDTO title2 = createMetadatumDTO("dc", "title", null, "Ischemic Heart Disease and Role of Nurse of Cardiology Department"); MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "Kozak, K. І."); - MetadatumDTO type2 = createMetadatumDTO("dc", "type", null, "journal-article"); + // is expected the dc.type to be mapped from journal-article to Article + MetadatumDTO type2 = createMetadatumDTO("dc", "type", null, "Article"); MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2016-05-19"); MetadatumDTO ispartof2 = createMetadatumDTO("dc", "relation", "ispartof", "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu"); diff --git a/dspace/config/crosswalks/mapConverter-crossref-to-dspace-publication-type.properties b/dspace/config/crosswalks/mapConverter-crossref-to-dspace-publication-type.properties new file mode 100644 index 000000000000..3bd4468148d6 --- /dev/null +++ b/dspace/config/crosswalks/mapConverter-crossref-to-dspace-publication-type.properties @@ -0,0 +1,29 @@ +# based on Crossref content type at https://crossref.gitlab.io/knowledge_base/docs/topics/content-types/#types-in-cayenne-rest-api +# Mapping between work type supported by Crossref and the DSpace common publication's types +journal-article = Article +journal-issue = Other +journal-volume = Other +journal = Other +proceedings-article = Other +proceedings = Other +dataset = Dataset +component = Other +report = Other +report-series = Other +standard = Other +standard-series = Other +edited-book = Other +monograph = Other +reference-book = Other +book = Book +book-series = Other +book-set = Other +book-chapter = Book chapter +book-section = Other +book-part = Other +book-track = Other +reference-entry = Other +dissertation = Other +posted-content = Other +peer-review = Other +other = Other \ No newline at end of file diff --git a/dspace/config/spring/api/crossref-integration.xml b/dspace/config/spring/api/crossref-integration.xml index 4786c44a7865..6fe9723d796b 100644 --- a/dspace/config/spring/api/crossref-integration.xml +++ b/dspace/config/spring/api/crossref-integration.xml @@ -49,9 +49,19 @@ + + + + + + + + + + From 268b5fc8b703a0fabfdb1ed8b3336e4bbca48ee2 Mon Sep 17 00:00:00 2001 From: Martin Walk Date: Thu, 13 Feb 2025 14:34:28 +0100 Subject: [PATCH 017/701] Fix #10405 bug in log4j-cli (cherry picked from commit 4c044adcf388a291dced77b53a9b816a6a11659c) --- dspace/config/log4j2-cli.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace/config/log4j2-cli.xml b/dspace/config/log4j2-cli.xml index 73acab877f1f..6cac529978fc 100644 --- a/dspace/config/log4j2-cli.xml +++ b/dspace/config/log4j2-cli.xml @@ -25,6 +25,7 @@ From b63329b45a758212048f235c9966ade1ba314021 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:25:23 +0000 Subject: [PATCH 018/701] Bump com.github.spotbugs:spotbugs-maven-plugin in the build-tools group Bumps the build-tools group with 1 update: [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin). Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.8.6.6 to 4.9.1.0 - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.8.6.6...spotbugs-maven-plugin-4.9.1.0) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 84bba3847341..0de103004d56 100644 --- a/pom.xml +++ b/pom.xml @@ -295,7 +295,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.8.6.6 + 4.9.1.0 Max Low From edbf9ef60598f40f0147f2563150d5a047623362 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:25:48 +0000 Subject: [PATCH 019/701] Bump commons-beanutils:commons-beanutils in the apache-commons group Bumps the apache-commons group with 1 update: commons-beanutils:commons-beanutils. Updates `commons-beanutils:commons-beanutils` from 1.10.0 to 1.10.1 --- updated-dependencies: - dependency-name: commons-beanutils:commons-beanutils dependency-type: direct:production update-type: version-update:semver-patch dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 84bba3847341..a6d2a6084637 100644 --- a/pom.xml +++ b/pom.xml @@ -1470,7 +1470,7 @@ commons-beanutils commons-beanutils - 1.10.0 + 1.10.1 commons-cli From 51c766caa32d73aa3d3415cccee59d175499506c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:26:49 +0000 Subject: [PATCH 020/701] Bump net.minidev:json-smart from 2.5.1 to 2.5.2 Bumps [net.minidev:json-smart](https://github.com/netplex/json-smart-v2) from 2.5.1 to 2.5.2. - [Release notes](https://github.com/netplex/json-smart-v2/releases) - [Commits](https://github.com/netplex/json-smart-v2/compare/2.5.1...2.5.2) --- updated-dependencies: - dependency-name: net.minidev:json-smart dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-server-webapp/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index f3fd8613fbf5..52105803889e 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -514,7 +514,7 @@ net.minidev json-smart - 2.5.1 + 2.5.2 From 1d85653ed0e524d95765bfbf64b18e1fd8436fda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:27:07 +0000 Subject: [PATCH 021/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.780 to 1.12.781 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.780 to 1.12.781. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.780...1.12.781) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 29b9865a5a7c..322e1820d325 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -761,7 +761,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.780 + 1.12.781 From 15dab1e41e3b0734e434c08e43db914750182a88 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Fri, 28 Feb 2025 11:27:36 +0100 Subject: [PATCH 022/701] Use NestableJsonFacet to process browse entries count response (cherry picked from commit 7ba09b7a857f6f3545d88b26940c1089692a860a) --- .../java/org/dspace/discovery/SolrServiceImpl.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index 34efe96ae7f8..29b695b5e785 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -41,6 +41,8 @@ import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.client.solrj.response.json.BucketBasedJsonFacet; +import org.apache.solr.client.solrj.response.json.NestableJsonFacet; import org.apache.solr.client.solrj.util.ClientUtils; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; @@ -1105,13 +1107,11 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query) */ private void resolveEntriesCount(DiscoverResult result, QueryResponse solrQueryResponse) { - Object facetsObj = solrQueryResponse.getResponse().get("facets"); - if (facetsObj instanceof NamedList) { - NamedList facets = (NamedList) facetsObj; - Object bucketsInfoObj = facets.get("entries_count"); - if (bucketsInfoObj instanceof NamedList) { - NamedList bucketsInfo = (NamedList) bucketsInfoObj; - result.setTotalEntries((int) bucketsInfo.get("numBuckets")); + NestableJsonFacet response = solrQueryResponse.getJsonFacetingResponse(); + if (response != null) { + BucketBasedJsonFacet facet = response.getBucketBasedFacets("entries_count"); + if (facet != null) { + result.setTotalEntries(facet.getNumBucketsCount()); } } } From 238893ce6dc5376338bf3add87fef288209bcd44 Mon Sep 17 00:00:00 2001 From: Zahraa Chreim Date: Tue, 11 Mar 2025 16:02:03 +0200 Subject: [PATCH 023/701] Fix invalid cast in DOIOrganiser exception handling --- .../dspace/identifier/doi/DOIOrganiser.java | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java index 507503ffaa15..1a142802d41a 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java @@ -448,28 +448,28 @@ public void register(DOI doiRow, Filter filter) + " is successfully registered."); } } catch (IdentifierException ex) { + String message; if (!(ex instanceof DOIIdentifierException)) { - LOG.error("It wasn't possible to register this identifier: " - + DOI.SCHEME + doiRow.getDoi() - + " online. ", ex); + message = "It wasn't possible to register this identifier: " + + DOI.SCHEME + doiRow.getDoi() + + " online. "; + } else { + DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; + message = "It wasn't possible to register this identifier : " + + DOI.SCHEME + doiRow.getDoi() + + " online. Exceptions code: " + + DOIIdentifierException.codeToString(doiIdentifierException.getCode()); } - DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; - try { sendAlertMail("Register", dso, DOI.SCHEME + doiRow.getDoi(), - doiIdentifierException.codeToString(doiIdentifierException - .getCode())); + message); } catch (IOException ioe) { LOG.error("Couldn't send mail", ioe); } - LOG.error("It wasn't possible to register this identifier : " - + DOI.SCHEME + doiRow.getDoi() - + " online. Exceptions code: " - + doiIdentifierException - .codeToString(doiIdentifierException.getCode()), ex); + LOG.error(message, ex); if (!quiet) { System.err.println("It wasn't possible to register this identifier: " @@ -541,27 +541,27 @@ public void reserve(DOI doiRow, Filter filter) { System.out.println("This identifier : " + DOI.SCHEME + doiRow.getDoi() + " is successfully reserved."); } } catch (IdentifierException ex) { + String message; if (!(ex instanceof DOIIdentifierException)) { - LOG.error("It wasn't possible to register this identifier : " - + DOI.SCHEME + doiRow.getDoi() - + " online. ", ex); + message = "It wasn't possible to register this identifier : " + + DOI.SCHEME + doiRow.getDoi() + + " online. "; + } else { + DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; + message = "It wasn't possible to reserve the identifier online. " + + " Exceptions code: " + + DOIIdentifierException.codeToString(doiIdentifierException.getCode()); } - DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; - try { sendAlertMail("Reserve", dso, DOI.SCHEME + doiRow.getDoi(), - DOIIdentifierException.codeToString( - doiIdentifierException.getCode())); + message); } catch (IOException ioe) { LOG.error("Couldn't send mail", ioe); } - LOG.error("It wasn't possible to reserve the identifier online. " - + " Exceptions code: " - + DOIIdentifierException - .codeToString(doiIdentifierException.getCode()), ex); + LOG.error(message, ex); if (!quiet) { System.err.println("It wasn't possible to reserve this identifier: " + DOI.SCHEME + doiRow.getDoi()); @@ -606,27 +606,27 @@ public void update(DOI doiRow) { + doiRow.getDoi() + "."); } } catch (IdentifierException ex) { + String message; if (!(ex instanceof DOIIdentifierException)) { - LOG.error("Registering DOI {} for object {}: the registrar returned an error.", - doiRow.getDoi(), dso.getID(), ex); + message = String.format("Registering DOI %s for object %s: the registrar returned an error.", + doiRow.getDoi(), dso.getID()); + } else { + DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; + message = "It wasn't possible to update this identifier: " + + DOI.SCHEME + doiRow.getDoi() + + " Exceptions code: " + + DOIIdentifierException.codeToString(doiIdentifierException.getCode()); } - DOIIdentifierException doiIdentifierException = (DOIIdentifierException) ex; - try { sendAlertMail("Update", dso, DOI.SCHEME + doiRow.getDoi(), - doiIdentifierException.codeToString(doiIdentifierException - .getCode())); + message); } catch (IOException ioe) { LOG.error("Couldn't send mail", ioe); } - LOG.error("It wasn't possible to update this identifier: " - + DOI.SCHEME + doiRow.getDoi() - + " Exceptions code: " - + doiIdentifierException - .codeToString(doiIdentifierException.getCode()), ex); + LOG.error(message, ex); if (!quiet) { System.err.println("It wasn't possible to update this identifier: " + DOI.SCHEME + doiRow.getDoi()); @@ -830,4 +830,4 @@ private void setQuiet() { this.quiet = true; } -} \ No newline at end of file +} From fbec7f2e56076de7575ea7404f9c36c47ef82539 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Fri, 21 Mar 2025 14:20:07 +0100 Subject: [PATCH 024/701] add missing whitespace in exception message (cherry picked from commit 9a904ab4c93775257f1e17d3bd3991e6c7257754) --- .../java/org/dspace/discovery/utils/DiscoverQueryBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java b/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java index 92a973dff883..b816e222539a 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java +++ b/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java @@ -302,7 +302,7 @@ private void configureSorting(String sortProperty, String sortDirection, Discove if (StringUtils.isNotBlank(sortBy) && !isConfigured(sortBy, searchSortConfiguration)) { throw new SearchServiceException( - "The field: " + sortBy + "is not configured for the configuration!"); + "The field: " + sortBy + " is not configured for the configuration!"); } From 00da667ba51362f134a4938514e3949c9bd60277 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Fri, 21 Mar 2025 14:34:04 +0100 Subject: [PATCH 025/701] add missing whitespace (cherry picked from commit 4ea49580937e60978035daf76b22588e3b979215) --- .../src/main/java/org/dspace/app/util/DCInputsReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index 8dc8239ca507..c77c3bf10e08 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java @@ -379,7 +379,7 @@ private void processRow(String formName, int rowIdx, Node n, List Date: Fri, 21 Mar 2025 19:01:09 +0000 Subject: [PATCH 026/701] Add bitstream null check to XOAI (cherry picked from commit 54602f47b1d35a7cb75b28acde08d43a93461e8a) --- dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java index 2d2679577802..16c16b0115b6 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java @@ -110,7 +110,7 @@ private List getFileFormats(Item item) { try { for (Bundle b : itemService.getBundles(item, "ORIGINAL")) { for (Bitstream bs : b.getBitstreams()) { - if (!formats.contains(bs.getFormat(context).getMIMEType())) { + if (bs != null && !formats.contains(bs.getFormat(context).getMIMEType())) { formats.add(bs.getFormat(context).getMIMEType()); } } From c797441b868d76f415e8df35191471ecc966720e Mon Sep 17 00:00:00 2001 From: DSpace Bot <68393067+dspace-bot@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:04:03 -0500 Subject: [PATCH 027/701] [Port dspace-7_x] Add null check in SolrServiceFileInfoPlugin for index-discovery (#10517) * Add null check in SolrServiceFileInfoPlugin for index-discovery (cherry picked from commit d07f1e0caad7632231a7a98cf2f2ff119d6a3b89) * Fix starting curly brace. (cherry picked from commit e11994c0ee75baf7389446e67ca8252f5f16a8d2) * Update SolrServiceFileInfoPlugin.java (cherry picked from commit 18372ae07246c5b0829286f7bb20263017528518) --------- Co-authored-by: jameswsullivan <81947235+jameswsullivan@users.noreply.github.com> --- .../discovery/SolrServiceFileInfoPlugin.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java index 7aece5acf313..6142dd0dba4b 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -52,21 +52,23 @@ public void additionalIndex(Context context, IndexableObject indexableObject, So List bitstreams = bundle.getBitstreams(); if (bitstreams != null) { for (Bitstream bitstream : bitstreams) { - document.addField(SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName()); - // Add _keyword and _filter fields which are necessary to support filtering and faceting - // for the file names - document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_keyword", bitstream.getName()); - document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_filter", bitstream.getName()); + if (bitstream != null) { + document.addField(SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName()); + // Add _keyword and _filter fields which are necessary to + // support filtering and faceting for the file names + document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_keyword", bitstream.getName()); + document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_filter", bitstream.getName()); - String description = bitstream.getDescription(); - if ((description != null) && !description.isEmpty()) { - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description); - // Add _keyword and _filter fields which are necessary to support filtering and - // faceting for the descriptions - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword", - description); - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter", - description); + String description = bitstream.getDescription(); + if ((description != null) && !description.isEmpty()) { + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description); + // Add _keyword and _filter fields which are necessary to support filtering and + // faceting for the descriptions + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword", + description); + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter", + description); + } } } } From 08e330c1c0a62ecfb83c2e7617472bd5a50f8c5a Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 26 Mar 2025 18:06:08 +0100 Subject: [PATCH 028/701] [DURACOM-243] Adds rotation handling inside JPEGFilter Conflicts: --- .../mediafilter/BrandedPreviewJPEGFilter.java | 23 +- .../dspace/app/mediafilter/JPEGFilter.java | 274 ++++++++++++++---- .../app/mediafilter/PDFBoxThumbnail.java | 1 + 3 files changed, 231 insertions(+), 67 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java index 7b082c6c21a4..483e4f5f6ea2 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java @@ -7,9 +7,7 @@ */ package org.dspace.app.mediafilter; -import java.awt.image.BufferedImage; import java.io.InputStream; -import javax.imageio.ImageIO; import org.dspace.content.Item; import org.dspace.services.ConfigurationService; @@ -63,27 +61,20 @@ public String getDescription() { @Override public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) throws Exception { - // read in bitstream's image - BufferedImage buf = ImageIO.read(source); - // get config params ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - float xmax = (float) configurationService - .getIntProperty("webui.preview.maxwidth"); - float ymax = (float) configurationService - .getIntProperty("webui.preview.maxheight"); - boolean blurring = (boolean) configurationService - .getBooleanProperty("webui.preview.blurring"); - boolean hqscaling = (boolean) configurationService - .getBooleanProperty("webui.preview.hqscaling"); + int xmax = configurationService.getIntProperty("webui.preview.maxwidth"); + int ymax = configurationService.getIntProperty("webui.preview.maxheight"); + boolean blurring = configurationService.getBooleanProperty("webui.preview.blurring"); + boolean hqscaling = configurationService.getBooleanProperty("webui.preview.hqscaling"); int brandHeight = configurationService.getIntProperty("webui.preview.brand.height"); String brandFont = configurationService.getProperty("webui.preview.brand.font"); int brandFontPoint = configurationService.getIntProperty("webui.preview.brand.fontpoint"); JPEGFilter jpegFilter = new JPEGFilter(); - return jpegFilter - .getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, - brandFont); + return jpegFilter.getThumb( + currentItem, source, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, brandFont + ); } } diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java index 502f71eb5ca8..181e3bcc4b58 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java @@ -8,22 +8,36 @@ package org.dspace.app.mediafilter; import java.awt.Color; +import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Transparency; +import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import javax.imageio.ImageIO; +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataException; +import com.drew.metadata.exif.ExifIFD0Directory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.w3c.dom.Node; /** * Filter image bitstreams, scaling the image to be within the bounds of @@ -33,6 +47,8 @@ * @author Jason Sherman jsherman@usao.edu */ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats { + private static final Logger log = LogManager.getLogger(JPEGFilter.class); + @Override public String getFilteredName(String oldFilename) { return oldFilename + ".jpg"; @@ -62,6 +78,134 @@ public String getDescription() { return "Generated Thumbnail"; } + /** + * Gets the rotation angle from image's metadata using ImageReader. + * This method consumes the InputStream, so you need to be careful to don't reuse the same InputStream after + * computing the rotation angle. + * + * @param buf InputStream of the image file + * @return Rotation angle in degrees (0, 90, 180, or 270) + */ + public static int getImageRotationUsingImageReader(InputStream buf) { + try { + Metadata metadata = ImageMetadataReader.readMetadata(buf); + ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { + return convertRotationToDegrees(directory.getInt(ExifIFD0Directory.TAG_ORIENTATION)); + } + } catch (MetadataException | ImageProcessingException | IOException e) { + log.error("Error reading image metadata", e); + } + return 0; + } + + public static int convertRotationToDegrees(int valueNode) { + // Common orientation values: + // 1 = Normal (0°) + // 6 = Rotated 90° CW + // 3 = Rotated 180° + // 8 = Rotated 270° CW + switch (valueNode) { + case 6: + return 90; + case 3: + return 180; + case 8: + return 270; + default: + return 0; + } + } + + /** + * Helper method to find a node with given name in metadata tree + */ + private static Node findNode(Node node, String name) { + if (node.getNodeName().equalsIgnoreCase(name)) { + return node; + } + + Node child = node.getFirstChild(); + while (child != null) { + Node found = findNode(child, name); + if (found != null) { + return found; + } + child = child.getNextSibling(); + } + return null; + } + + /** + * Rotates an image by the specified angle + * + * @param image The original image + * @param angle The rotation angle in degrees + * @return Rotated image + */ + public static BufferedImage rotateImage(BufferedImage image, int angle) { + if (angle == 0) { + return image; + } + + double radians = Math.toRadians(angle); + double sin = Math.abs(Math.sin(radians)); + double cos = Math.abs(Math.cos(radians)); + + int newWidth = (int) Math.round(image.getWidth() * cos + image.getHeight() * sin); + int newHeight = (int) Math.round(image.getWidth() * sin + image.getHeight() * cos); + + BufferedImage rotated = new BufferedImage(newWidth, newHeight, image.getType()); + Graphics2D g2d = rotated.createGraphics(); + AffineTransform at = new AffineTransform(); + + at.translate(newWidth / 2, newHeight / 2); + at.rotate(radians); + at.translate(-image.getWidth() / 2, -image.getHeight() / 2); + + g2d.setTransform(at); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + + return rotated; + } + + /** + * Calculates scaled dimension while maintaining aspect ratio + * + * @param imgSize Original image dimensions + * @param boundary Maximum allowed dimensions + * @return New dimensions that fit within boundary while preserving aspect ratio + */ + private Dimension getScaledDimension(Dimension imgSize, Dimension boundary) { + + int originalWidth = imgSize.width; + int originalHeight = imgSize.height; + int boundWidth = boundary.width; + int boundHeight = boundary.height; + int newWidth = originalWidth; + int newHeight = originalHeight; + + + // First check if we need to scale width + if (originalWidth > boundWidth) { + // Scale width to fit + newWidth = boundWidth; + // Scale height to maintain aspect ratio + newHeight = (newWidth * originalHeight) / originalWidth; + } + + // Then check if we need to scale even with the new height + if (newHeight > boundHeight) { + // Scale height to fit instead + newHeight = boundHeight; + newWidth = (newHeight * originalWidth) / originalHeight; + } + + return new Dimension(newWidth, newHeight); + } + + /** * @param currentItem item * @param source source input stream @@ -72,10 +216,59 @@ public String getDescription() { @Override public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) throws Exception { + return getThumb(currentItem, source, verbose); + } + + public InputStream getThumb(Item currentItem, InputStream source, boolean verbose) + throws Exception { + // get config params + final ConfigurationService configurationService + = DSpaceServicesFactory.getInstance().getConfigurationService(); + int xmax = configurationService + .getIntProperty("thumbnail.maxwidth"); + int ymax = configurationService + .getIntProperty("thumbnail.maxheight"); + boolean blurring = (boolean) configurationService + .getBooleanProperty("thumbnail.blurring"); + boolean hqscaling = (boolean) configurationService + .getBooleanProperty("thumbnail.hqscaling"); + + return getThumb(currentItem, source, verbose, xmax, ymax, blurring, hqscaling, 0, 0, null); + } + + protected InputStream getThumb( + Item currentItem, + InputStream source, + boolean verbose, + int xmax, + int ymax, + boolean blurring, + boolean hqscaling, + int brandHeight, + int brandFontPoint, + String brandFont + ) throws Exception { + + File tempFile = File.createTempFile("temp", ".tmp"); + tempFile.deleteOnExit(); + + // Write to temp file + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + byte[] buffer = new byte[4096]; + int len; + while ((len = source.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + } + + int rotation = getImageRotationUsingImageReader(new FileInputStream(tempFile)); // read in bitstream's image - BufferedImage buf = ImageIO.read(source); + BufferedImage buf = ImageIO.read(new FileInputStream(tempFile)); - return getThumb(currentItem, buf, verbose); + return getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, rotation, + brandFont + ); } public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose) @@ -83,25 +276,28 @@ public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose // get config params final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - float xmax = (float) configurationService + int xmax = configurationService .getIntProperty("thumbnail.maxwidth"); - float ymax = (float) configurationService + int ymax = configurationService .getIntProperty("thumbnail.maxheight"); boolean blurring = (boolean) configurationService .getBooleanProperty("thumbnail.blurring"); boolean hqscaling = (boolean) configurationService .getBooleanProperty("thumbnail.hqscaling"); - return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, 0, 0, null); + return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, 0, 0, 0, null); } - public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verbose, float xmax, float ymax, + public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verbose, int xmax, int ymax, boolean blurring, boolean hqscaling, int brandHeight, int brandFontPoint, - String brandFont) + int rotation, String brandFont) throws Exception { - // now get the image dimensions - float xsize = (float) buf.getWidth(null); - float ysize = (float) buf.getHeight(null); + + // Rotate the image if needed + BufferedImage correctedImage = rotateImage(buf, rotation); + + int xsize = correctedImage.getWidth(); + int ysize = correctedImage.getHeight(); // if verbose flag is set, print out dimensions // to STDOUT @@ -109,77 +305,53 @@ public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verb System.out.println("original size: " + xsize + "," + ysize); } - // scale by x first if needed - if (xsize > xmax) { - // calculate scaling factor so that xsize * scale = new size (max) - float scale_factor = xmax / xsize; + // Calculate new dimensions while maintaining aspect ratio + Dimension newDimension = getScaledDimension( + new Dimension(xsize, ysize), + new Dimension(xmax, ymax) + ); - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println("x scale factor: " + scale_factor); - } - - // now reduce x size - // and y size - xsize = xsize * scale_factor; - ysize = ysize * scale_factor; - - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println("size after fitting to maximum width: " + xsize + "," + ysize); - } - } - - // scale by y if needed - if (ysize > ymax) { - float scale_factor = ymax / ysize; - - // now reduce x size - // and y size - xsize = xsize * scale_factor; - ysize = ysize * scale_factor; - } // if verbose flag is set, print details to STDOUT if (verbose) { - System.out.println("size after fitting to maximum height: " + xsize + ", " - + ysize); + System.out.println("size after fitting to maximum height: " + newDimension.width + ", " + + newDimension.height); } + xsize = newDimension.width; + ysize = newDimension.height; + // create an image buffer for the thumbnail with the new xsize, ysize - BufferedImage thumbnail = new BufferedImage((int) xsize, (int) ysize, - BufferedImage.TYPE_INT_RGB); + BufferedImage thumbnail = new BufferedImage(xsize, ysize, BufferedImage.TYPE_INT_RGB); // Use blurring if selected in config. // a little blur before scaling does wonders for keeping moire in check. if (blurring) { // send the buffered image off to get blurred. - buf = getBlurredInstance((BufferedImage) buf); + correctedImage = getBlurredInstance(correctedImage); } // Use high quality scaling method if selected in config. // this has a definite performance penalty. if (hqscaling) { // send the buffered image off to get an HQ downscale. - buf = getScaledInstance((BufferedImage) buf, (int) xsize, (int) ysize, - (Object) RenderingHints.VALUE_INTERPOLATION_BICUBIC, (boolean) true); + correctedImage = getScaledInstance(correctedImage, xsize, ysize, + RenderingHints.VALUE_INTERPOLATION_BICUBIC, true); } // now render the image into the thumbnail buffer Graphics2D g2d = thumbnail.createGraphics(); - g2d.drawImage(buf, 0, 0, (int) xsize, (int) ysize, null); + g2d.drawImage(correctedImage, 0, 0, xsize, ysize, null); if (brandHeight != 0) { ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - Brand brand = new Brand((int) xsize, brandHeight, new Font(brandFont, Font.PLAIN, brandFontPoint), 5); + Brand brand = new Brand(xsize, brandHeight, new Font(brandFont, Font.PLAIN, brandFontPoint), 5); BufferedImage brandImage = brand.create(configurationService.getProperty("webui.preview.brand"), configurationService.getProperty("webui.preview.brand.abbrev"), currentItem == null ? "" : "hdl:" + currentItem.getHandle()); - g2d.drawImage(brandImage, (int) 0, (int) ysize, (int) xsize, (int) 20, null); + g2d.drawImage(brandImage, 0, ysize, xsize, 20, null); } // now create an input stream for the thumbnail buffer and return it diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java index 3acb6900dbda..577f1dec4a18 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java @@ -81,6 +81,7 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo // Generate thumbnail derivative and return as IO stream. JPEGFilter jpegFilter = new JPEGFilter(); + return jpegFilter.getThumb(currentItem, buf, verbose); } } From 82d04061c084486b92c9f643b96f59ab81fd4a87 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 27 Mar 2025 12:38:59 +0100 Subject: [PATCH 029/701] [DURACOM-243] Adds Test for JPEGFilter --- .../dspace/app/mediafilter/JPEGFilter.java | 53 ++-- .../app/mediafilter/JPEGFilterTest.java | 270 ++++++++++++++++++ .../dspace/app/mediafilter/cat-rotated-90.jpg | Bin 0 -> 36813 bytes .../org/dspace/app/mediafilter/cat.jpg | Bin 0 -> 36813 bytes 4 files changed, 290 insertions(+), 33 deletions(-) create mode 100644 dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/cat.jpg diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java index 181e3bcc4b58..2ccc2afbb2d2 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java @@ -37,7 +37,6 @@ import org.dspace.content.Item; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.w3c.dom.Node; /** * Filter image bitstreams, scaling the image to be within the bounds of @@ -117,25 +116,6 @@ public static int convertRotationToDegrees(int valueNode) { } } - /** - * Helper method to find a node with given name in metadata tree - */ - private static Node findNode(Node node, String name) { - if (node.getNodeName().equalsIgnoreCase(name)) { - return node; - } - - Node child = node.getFirstChild(); - while (child != null) { - Node found = findNode(child, name); - if (found != null) { - return found; - } - child = child.getNextSibling(); - } - return null; - } - /** * Rotates an image by the specified angle * @@ -261,14 +241,20 @@ protected InputStream getThumb( } } - int rotation = getImageRotationUsingImageReader(new FileInputStream(tempFile)); - // read in bitstream's image - BufferedImage buf = ImageIO.read(new FileInputStream(tempFile)); + int rotation = 0; + try (FileInputStream fis = new FileInputStream(tempFile)) { + rotation = getImageRotationUsingImageReader(fis); + } - return getThumbDim( - currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, rotation, - brandFont - ); + try (FileInputStream fis = new FileInputStream(tempFile)) { + // read in bitstream's image + BufferedImage buf = ImageIO.read(fis); + + return getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, rotation, + brandFont + ); + } } public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose) @@ -354,13 +340,14 @@ public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verb g2d.drawImage(brandImage, 0, ysize, xsize, 20, null); } - // now create an input stream for the thumbnail buffer and return it - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - ImageIO.write(thumbnail, "jpeg", baos); - // now get the array - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ByteArrayInputStream bais; + // now create an input stream for the thumbnail buffer and return it + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + ImageIO.write(thumbnail, "jpeg", baos); + // now get the array + bais = new ByteArrayInputStream(baos.toByteArray()); + } return bais; // hope this gets written out before its garbage collected! } diff --git a/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java b/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java new file mode 100644 index 000000000000..1181dc7a60f0 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java @@ -0,0 +1,270 @@ +/** + * 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.app.mediafilter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.dspace.AbstractUnitTest; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Test; +import org.mockito.Mock; + +public class JPEGFilterTest extends AbstractUnitTest { + + @Mock + private ConfigurationService mockConfigurationService; + + @Mock + private DSpaceServicesFactory mockDSpaceServicesFactory; + + @Mock + private InputStream mockInputStream; + + @Mock + private Item mockItem; + + /** + * Tests that the convertRotationToDegrees method returns 0 for an input value + * that doesn't match any of the defined rotation cases. + */ + @Test + public void testConvertRotationToDegrees_UnknownValue_ReturnsZero() { + int result = JPEGFilter.convertRotationToDegrees(5); + assertEquals(0, result); + } + + /** + * Test getNormalizedInstance method with a null input. + * This tests the edge case of passing a null BufferedImage to the method. + * The method should throw a NullPointerException when given a null input. + */ + @Test(expected = NullPointerException.class) + public void testGetNormalizedInstanceWithNullInput() { + JPEGFilter filter = new JPEGFilter(); + filter.getNormalizedInstance(null); + } + + /** + * Test getThumbDim method with a null BufferedImage input. + * This tests the edge case where the input image is null, which should result in an exception. + */ + @Test(expected = NullPointerException.class) + public void testGetThumbDimWithNullBufferedImage() throws Exception { + JPEGFilter filter = new JPEGFilter(); + Item currentItem = null; + BufferedImage buf = null; + boolean verbose = false; + int xmax = 100; + int ymax = 100; + boolean blurring = false; + boolean hqscaling = false; + int brandHeight = 0; + int brandFontPoint = 0; + int rotation = 0; + String brandFont = null; + + filter.getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, + brandHeight, brandFontPoint, rotation, brandFont + ); + } + + /** + * Tests that the rotateImage method returns the original image when the rotation angle is 0. + * This is an edge case explicitly handled in the method implementation. + */ + @Test + public void testRotateImageWithZeroAngle() { + BufferedImage originalImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + BufferedImage rotatedImage = JPEGFilter.rotateImage(originalImage, 0); + assertSame( + "When rotation angle is 0, the original image should be returned", + originalImage, rotatedImage + ); + } + + /** + * Test case for convertRotationToDegrees method when input is 6. + * Expected to return 90 degrees for the rotation value of 6. + */ + @Test + public void test_convertRotationToDegrees_whenInputIs6_returns90() { + int input = 6; + int expected = 90; + int result = JPEGFilter.convertRotationToDegrees(input); + assertEquals(expected, result); + } + + /** + * Tests that getBlurredInstance method applies a blur effect to the input image. + * It verifies that the returned image is not null, has the same dimensions as the input, + * and is different from the original image (indicating that blurring has occurred). + */ + @Test + public void test_getBlurredInstance_appliesBlurEffect() { + JPEGFilter filter = new JPEGFilter(); + BufferedImage original = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + + BufferedImage blurred = filter.getBlurredInstance(original); + + assertNotNull("Blurred image should not be null", blurred); + assertEquals("Width should be the same", original.getWidth(), blurred.getWidth()); + assertEquals("Height should be the same", original.getHeight(), blurred.getHeight()); + assertNotEquals("Blurred image should be different from original", original, blurred); + } + + /** + * Test case for getBundleName method of JPEGFilter class. + * This test verifies that the getBundleName method returns the expected string "THUMBNAIL". + */ + @Test + public void test_getBundleName_returnsExpectedString() { + JPEGFilter filter = new JPEGFilter(); + String result = filter.getBundleName(); + assertEquals("THUMBNAIL", result); + } + + /** + * Tests that the getDescription method returns the expected string "Generated Thumbnail". + * This verifies that the method correctly provides the description for the JPEG filter. + */ + @Test + public void test_getDescription_1() { + JPEGFilter filter = new JPEGFilter(); + String description = filter.getDescription(); + assertEquals("Generated Thumbnail", description); + } + + /** + * Tests that getFilteredName method appends ".jpg" to the input filename. + */ + @Test + public void test_getFilteredName_appendsJpgExtension() { + JPEGFilter filter = new JPEGFilter(); + String oldFilename = "testimage"; + String expectedResult = "testimage.jpg"; + String actualResult = filter.getFilteredName(oldFilename); + assertEquals(expectedResult, actualResult); + } + + /** + * Test case for getFormatString method of JPEGFilter class. + * Verifies that the method returns the expected string "JPEG". + */ + @Test + public void test_getFormatString_returnsJPEG() { + JPEGFilter filter = new JPEGFilter(); + String result = filter.getFormatString(); + assertEquals("JPEG", result); + } + + /** + * Tests the behavior of getImageRotationUsingImageReader when an ImageProcessingException occurs. + * This test verifies that the method handles an ImageProcessingException by logging the error + * and returning 0 degrees rotation. + */ + @Test + public void test_getImageRotationUsingImageReader_imageProcessingException() { + InputStream errorStream = new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("Simulated image processing error"); + } + }; + int result = JPEGFilter.getImageRotationUsingImageReader(errorStream); + assertEquals(0, result); + } + + /** + * Testcase for getImageRotationUsingImageReader when the image doesn't contain orientation metadata. + * This test verifies that the method returns 0 when there's no ExifIFD0Directory + * or when it doesn't contain the TAG_ORIENTATION. + */ + @Test + public void test_getImageRotationUsingImageReader_noOrientationMetadata() throws IOException { + URL resource = this.getClass().getResource("cat.jpg"); + int rotationAngle = -1; + try (InputStream inputStream = new FileInputStream(resource.getFile())) { + // Call the method under test + rotationAngle = JPEGFilter.getImageRotationUsingImageReader(inputStream); + } + assertEquals(0, rotationAngle); + } + + /** + * Tests the getImageRotationUsingImageReader method when the image contains + * valid EXIF orientation metadata. + * + * This test verifies that the method correctly reads the orientation tag + * from the EXIF metadata and returns the appropriate rotation angle in degrees. + */ + @Test + public void test_getImageRotationUsingImageReader_withValidExifOrientation() throws Exception { + // Create a mock InputStream with EXIF metadata containing orientation information + URL resource = this.getClass().getResource("cat-rotated-90.jpg"); + int rotationAngle = -1; + try (InputStream inputStream = new FileInputStream(resource.getFile())) { + // Call the method under test + rotationAngle = JPEGFilter.getImageRotationUsingImageReader(inputStream); + } + + // Assert the expected rotation angle + // Note: The expected value should be adjusted based on the mock data + assertEquals(90, rotationAngle); + } + + /** + * Tests the getScaledInstance method of JPEGFilter class with higher quality scaling. + * This test verifies that the method correctly scales down an image in multiple passes + * when higherQuality is true and the image dimensions are larger than the target dimensions. + */ + @Test + public void test_getScaledInstance() { + JPEGFilter filter = new JPEGFilter(); + BufferedImage originalImage = new BufferedImage(400, 300, BufferedImage.TYPE_INT_RGB); + int targetWidth = 100; + int targetHeight = 75; + Object hint = RenderingHints.VALUE_INTERPOLATION_BILINEAR; + boolean higherQuality = true; + + BufferedImage result = filter.getScaledInstance(originalImage, targetWidth, targetHeight, hint, higherQuality); + + assertNotNull(result); + assertEquals(targetWidth, result.getWidth()); + assertEquals(targetHeight, result.getHeight()); + } + + /** + * Tests the rotateImage method with a non-zero angle. + * This test verifies that the image is rotated correctly when given a non-zero angle. + */ + @Test + public void test_rotateImage_nonZeroAngle() { + BufferedImage originalImage = new BufferedImage(100, 50, BufferedImage.TYPE_INT_RGB); + int angle = 90; + + BufferedImage rotatedImage = JPEGFilter.rotateImage(originalImage, angle); + + assertNotNull(rotatedImage); + assertEquals(50, rotatedImage.getWidth()); + assertEquals(100, rotatedImage.getHeight()); + } + +} diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg b/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5c0f91c4eda737b23e82a092905cacbf9dc56f29 GIT binary patch literal 36813 zcmeFZXIKAj+Ii}DVLV}iR=yZK2Iq_M6V$U&5x78g%*e>h!n})xWfvz43kxUd#=^Ok zh3$VLfcOkxqXQ5?IweF1pkRYgvO$QwU?Ulbbr3DEmedgP1y-1JQ34PODry>9I(i01 z$lr+&fP!*|xC?+#K`1GxD5z;^=%^`~*N!f7RB@;6(;rX?K&|SJ?MQ7hX0K2ImdMu4P7) zUaxBzTF5GU)jGUr?;UV4DZBi2+sKl#k%LcQpIb`G(Z{$!j)VsE2w zPg|v^<>8jo_iXtZ1DP2M0@489>WZA{i!yVETWfMYgz=WpHigE2&5C;!=fZ!c+P~MJ zsEvc~aJUfb?I=uX!rfTqUox2$&e^9cw`gOG73yVHBJ!Sg6 z-_NWEQp)YufBNFX)f~@~keRmknpZtu?e7eJMjLkf9?dhO;uQzA&bapQc4;DTR{IU} zQGK&m2&{X(x5_>Jp^C~Gh3O}oF^io~^^V+hx_O>{dCfNzwdg!LKdwfw4MB(h&>G&1 z#iRTZA+7uOOX`#zr4fzF7KFe>^fzsxFdx05b9_B*Y_;)RuA_=^eV;NmYkOh~kU+Bs zU>V*JIsUlo1j}bNORdQ#4uxw55%uM1QJX_X&Jt9mOKqCQ9dzNu&!ZtXD%jNzQ_eU^ z^0yYXmZryxMkK66t9*Ma*{gZy^*dNdeQOf;!U+#H!L;h@!zz?L{pS`xo|#p3l@EfP zqfJZ{%~*scO*K8ki-+Ff)nU(cZa>NK68b^4(elV*MP~br3pnb~Uekafdv5`4wV3kw z*_&yf?AGI*us8c1Lq_$~WaU^a4%1c$h%FQdRJK*&Q28sGDXBNsc^5{frv$DCW7_yr zdzh+bBaRgC6!6;B&}dCfq*25V!EaKlzr0 z)a-Ly=kdO2AoTVptsFIkYu%wm(@Xv>DO%^Q(tKZ)o!7d%X!y1J_}vSK(~ci6(Ud9d z`7Ay-_Tp2?>&JDz*$QpPr?`@)csaECZt@*s@k18(g)9?+w+DI8$Di4Ipr(0M!_-Ji zdVLtsbt5*8R=$mXvuUI4bZkoCKvByiVH9;L@-1yC>qpzuudaW}#tRv(ZXDQaoNYoG zs$3ZgoBAPbJ&t{^s@>eN`xbsl#rd-itq!g3Q#Be5-mYF_-{zsLxcAuJme;sgEWwKU z4p*0_8mu7rJ4YSgBxoLIU&GV@XPkf4UxWCvb zs%ZEQ0bgeG>z!jpbTw~=G`$-qG#SJ~@0_5Phz%$$Wh^{J1Tq$CkMQyB-cuejkoAcW z@$#!SI;|7kayqNUf3CP-F{L65ulN>x3yTcj;H*O;nMzHjWX&t_!f0q9lSMA zkpEdl0n0~o|N$Yc%~bjMr<1DM|656 zdVLo&0(E&UyQhlBU1du3jK$S@eA}vn{oYaEwMU!GcK%d=VU1gKPTldi$SLGS z1!O>z*p|pn zZ{Ky)@`=$SLpQ2=zM2G#H-79HW|$!;m0p#UVYIksSbeZ#Bs5U+dA+FbcQ9hFv+; z@ut|UGE|ypL2d)hRoX*4lQ4}=ZQ1ZlpK!V|AeniJ=}p(!06kUo9)#P(8nqqA!ksbc zTJd+`1y!E!$2T0kdY8=RCp@lDp4jA_Td@2NPP$fSjs3pWyi3{bIw660dApsxZ8iMy z>nKd4v*MBE+7|DLJ*DTqe^tJ9WyW;u8)jxh&B3UpGA!wsLZ$1=0~d@n`Rr-$LS21& zf>l4JDP&11FkB{7xhAJMNz}sAGTd)!wa4bQvLN2Jrxg_3b*BjRf z1^#^Edm|}x7Ee6glkMEx#o6;vGr#+&#|WLeVojbJUDNXwIMl#sdZE>B==e-h{>^e{~Pv*S)K>Uy6D< zXi45hTdZ%?Vx+kChuHVSmKW?3d!M}_SV{zzKcOyX=lwiG@wFl(Vxc{nvS*~%U8w3M zeaDYYLYfgSToI>uW_dMU?ah~~W%m?2T{{*gpy8%rMBvGK`>KKWowgz6rbi1G!q{gr zq04$Z(r(`8V((^bYfn0<;UD;Uy^Gp3S#K2`X=&GUd0gdMKdYyIvj2hA>(x*_ zogucIrRzQFr)=DV-K9oC;V<6oe)9VG{+y~H=U$cgOtWkH$on@kUQz&iuU9swL$D46 z`!B^2l|56A9a1t!0$y(%Ku}KFh2C;Ct9d7KQ6coDVdUxFGWiwPoK-c>xw{!`kMbK; zL+Ov5N}AZuNSWt8qN0@Z+>g2{!=-rv+TGo`nQ)>cQAa-KHntnS%S#9qjEv(@i0!)9 z_5(hvYrsteh%NKwH7B%}0d+QSCz0o0mIuf3&Ybz8)8>^HU8WaktKG&@yK-Yx8GGj9 zo5Rz5d|kn^T3YLg0o4gbEb8FQYr0}=?})#YsMv0!)oP7!qtQ}TsxM9Wq$Sd2{^%m| zKppdIS71naKCr5*zlR89pM`6F@phxGGknzTcJI@@LMKNBm9wRe15zo*-&)P5dUU)W z@i{AsxvkBN|POJjHB1I~ALVWpq&DApzTuq1f2?AUHxV55EWSb6T&>nX&ihA3a;OSK!zgtyaHC zfvl;_wT-y-HKh3%&9Ll@R`vrFG%97^LQlg+?dXPrw`#H!=b1(?bWi7bU~GJ2W}R6| z($2hVx~!9FQMPAa{`q$h8KBpugm-z>KJ;nro8ue8d1_PP(EgfaHO&`p$IrGtoa)c$ zUN6di^4`M~k^Abxlfu?Q{E_rE!*&KcV_EiGWY@5)rsl^d%(JV&*7e^*9lj)v}zo zmFv8ts3AOo0-g)&5rNa!y`yQqnC-4IKfZo!Y#|IM;PZtDWW}$#-6?w1yfC>5O<0?o zH$Tzd@Dg|4#D9OyC}eOx4R@XK;WYE&;rq$FUOcy^#^;`N-#C{2e7(PEMwSSq2(@c| z%IXtRA_6dGlncL&K=!Efa=W40QZ+5bCghgdgQX48e7$s^F+>CH6S1t z>$Ob$uV|ahm*n<7U-jQh1jOcBL!sZM(=3$ZG$S_zLeROGD(f#xH$qTXgnU*cA$Du7 zj2a82N9JhC&k<&q4>4DDR^Z3^F_pCvZR^C@r5d*+DF2D+hxADYakYo-MG#&;+6Hp`q2_`-DkXTZhm7LcG9z^2Mh2u$i zNaQV0vw5lqsbycIz5?N&)ms$OB-q=5fqZ(?N^K0e`~R& zA0CcD<3Rdu5~rRY9=+2Du#ukFp9yOpocYg$9u}?lD}l!UN|<>faX%>*KKOv22?G>T z|7QZ!<1f*=P=wF6BDS;ySR3i;fwiF|Rms{*P|yoQ@YKe9AtlL|5f=RqI9=pF^66r| zZ0*o^Lt$$qsc(N?x=1g01S%9VD&xl9;6nKD{19ph1;$SpQLI7W-#w9F@G!87W8lavtn(a$Su4T7OL$;l0d$8L9rM=2VWT2xuAu@(LOus zV<8c&!8++;@OTUgi9!2pXJOi*kgD(-o_Pmv4fpZ=oxr$50PFUXpEOn|r?%>_?em1} zJjvroluTpUE);xdodLG&jhT!id3^cFoO}M7rkp3I>B)ES1|jDHy}_i5VSA8(XmSk} z$yj4R+S?nXAi+E6iv};qHs^i z|Fj&41em13A>mLM&K_w-vVF*3%AqFHKr}n}M@vqa=>Jw0tq&GM@cRpj27`tBz<*9_ z1~w$=mSh|#gV}?C#~8uTFsuh2<^>`E+hD()S(#yrnS>!xOi(_8;N$~^z<`G*=&%$7!ympNj0gX4WC(Q70v!K=LlDpdLnTTifn?7RYdQ!5G~6hRAIaHI z@{p1WB3-oL-xdtscf7mXI0z|CV*2eO^X}lOc7T8NBxNK?`1AKi2Y+nf|4keCee$>l zCVoY1SZqD8J}^8O?j?orNU;)f^vfKN*ZWJ%gu%iC4-bGHBY2SF8|2U@DWtg_H6hPX zq_F4jX;NtP_cR#HMeT$SfMZB}@HUY&oiTVK30NHz7!$&iqNyNe=VFDSJYilYdU{|C z*&B|8?L-+FNC`s>76pb}w!+^GhCeZ+%tx&ZjKBvI8e{H($HT}0f}PA@1RUdoh2d~u zlXh@SAz;+N3v3_~jt~Az+v%N26ce;J29!q%PSHbsJdKPj@v@r#$N0bL{k_FJcL{*7#e1;bXq(b@k-w}yHBM*Vwk8$8$< z+jLT|{#LjN%EJd{<$?G8OT38F4hJJsba1~4+EE=7sj2_Af!tsjTsp`=k6@gRmzNF_ zuY<>9;hqFMxl>6sVE&2G1EZ1KRU=t1lO7U-gXw^wEf1fceL`n|hGM+Haj?w@@&W85 ztFrM30I~nJE({d_jDQ`BB598K7pu7kbf>deeqrq_OdNO6zscR+HASM3ZDVUqi#Zqxg!_Rb z8Pv?re&E^C_rKHz{dVgi+fUBz=Z0A>+7SV4i`)^tcRT$L zqCt^tJ68Qu_dAUyQOv;g5C-6q95jx!v}H$8yNobq;AFJZ1;6IGUF2Rg_wX~rfEEO1 z*)Ev0osDUSMv91T<3TjQYJ!F$30@>=D3THF6wkhmcZOlXNf8SMw11LlOwe8+D@n-q ze8y-7#=8yBq~UE~$sDVNR0&jSj>;tWuJ_+ULS zl)NL0ly8R!hKJ$c?2Gq6ZWlzF=~%aOl7hkD;Q5`zWCOlDV7RT>|F?EC+v+>Oz3{%q zu;12^kc^EM9OZwT0BFFj(6h1G(LZS>G6Ne=YSz!*+0h%fISlXN1+K5zYWu&P2S^Jd z7{S>{kAMSLD41C9$e;yr2973%J82p)?PzYbQ$EL5(gs7oLdk={&e~*$#z>yItuWvm z^7}iJWv5nup^*BB)d35~!_47mILZULT?l!OA>%=WEyf1!1%rBE|1#`oK~P#fdawR94MSZAo!#p|61BwvqB5L48d)95P&$qCgh99`>9Gxqj6Fmq&<^TPz*{s z$iq)sR!T-1P}d3q%>V(z3wpvptJ4sgsj3wcgnMZSIVhUTnEUC$eBs6+SeQ+Sg)KA$ z0X^a+q@^jS9;6zC@;55P5H-AlNoRJq#8m zs3;{b36+tR6;x7^l2uStQj(Pr1h;F-Ny~u$l_bG^n~F-Ra>|0+mk?MQ*2`PfTHkQH zEHI_9)2P6}K&e1^DGb&}TK341BP1Dea*`m2BrX`V+8{|ZPIyOxJ`4xN!pZB_1W6LX zHCY6_h7efOttO!Sev194y4qp%g8t<51J`np3-^Lb!;moYkOuWCOKLt@sNc^1)Sp@Y zBKjwp+!;HWK-HR?|MLtx{f0vQECPo&2mqV$?|K;+ZzCV!P`2!W%|2PS>e z59T8z__GgG$%7dz8LXcC@AzMzxEew@aEYOyiGiMy>=7j;NjWK5a(&IsRZY-1yayTz zGtt)&0vjs@hkL2Y8!G6@=&R`IACc2FP>_|?QPNc~R5ma)P?1&ESJKnn$*Yfn5=geQ zlh=!s_sIV&@77W$59EI(gEW#!#-?fv2j@g=@b*}u>4Hkfdz#TR`=nj^_V?J%O(TRj>bQ4%;esYYFFnIfs$# zf!x^N-tRjo@S}kQ{`^|e-1(~b^ZcWMKN|R>fj=7fqk%sf_W za2*Io1rSJkV<^o)_+kJY26~eyTR>O@NkG9tn6&qa69w}C9n$1o6C!vR)E9(hKs*!H z)>;pQ)j>xTlh1Fk=Wj3`47-550LckY-nJol5PC=ubl9i}8p8sSV1!H33UoqwV7)*W z5$Neg2Lr%Xn@PC<_J8=%O-66s{w?u0zJCXj`gV&xe~jE`5bpQCaKCc?!eKz)n+mwk zjPlAaoaZe7sE7ano~~awk#x}KcL4y(KmFDoZj!xh`)f!JEI14*MN;U`>pwF5Fane( zQb7vjXTO=z+k;C>0Kh?Dbj$8R7^IW$f4Nb_gGmY;oV;@k(6hD`gc7g;TP#$h2Q|1m zkrm(s_JAkc2?GZKNk9%z2Gjs;zyLT3SOT_y6W|I!!Nn^maQ|H(a0)mJTm)i(E5LQ& zHjoBn0=YmTPy$o{)xdM$HSiAj4738>z!zW`m;`2lC14Fahm97(4B>$6fe1hjKn_FX zASw_|hylb5VhwSExI=s(Xb1sv3UVG214)G3hTMhZLW&_zAkQIhA&rnu$N*#<@*T2D zK}o?x!AY@?LX<+9LWM$y!j!_6!iBS5|78X6jI8ZjDW8eOshd_ zN$Ww2r@csfi}nF+Eo~F+FzpXIMmhmHIXXi+XF4?9Il3geJh~dXCc06&HTqrj2k2Gl zE$O}JgX!bxGwCboKhY1-b^V2WePVS2{Y$u!T*%q+^R&FsvKXO3geWq!%r$NYnZ zgGGwPgvE;`oF$p1oTZ6nnw6gQ0ILqGD{BaA66+(@Ppsc|(eD!7rMJst*Xdo!yPoW7 z-?hlb!6wUQ$%bOP%$CRYmTiokmR*$HfZdDz0(%DgOZGtyN)BNTJq{?xd5%nuR~*Bf zG@N3bCY&cZV>k;qKXT4;v2!VK*>eSPC38LH`oc}gEy``ejpUB!F5zzDUfsQKx6W?Z z?n}E1cQ@@`=HcVf=7I4<^Az#4@~rL=++(l@u_s|q#hzYXDqeBkW4wX9sl2avzwvSM zsqsPiqWMbry7p4+72j*UH*{~t-uHVK_wnyD+UK|L*1nhfzVUPOYxAGvzs6s~KQ6!_ zpeXdMbQ87^m(MZt>(J?U|F%z*6v3#+fgUkoD z4*DOwcd+FU%^{UTh(jrdK8sU`D~kJyCyReNOmSH0F#K@J;YJB+i6atdiF*?5l1!31 zk^z$Wk^@rQQf5-;q$;GQrA4Hjr4yvF;Rp?OUP&8MJRD7Yhp`@&YRVq{(R~AurSH7d%uEMEerE*#2 z%@OJ&x<|r~R3BMURZ_*PmZ;9CNva{#^3=xE#nfTynd*ZYLK+?#cQw9f3TnD*rfYuD z+OOrIbx-T7wy?IBcDDAY&LN$XIuCTFb!Bw1x@Ee{da8P1de8JJ^^NqS^*J+ooU5#Lci~ zmF5)YN6oLA_gEaTKwDH;0+uG0S1o(3#H{?Ss*cedvpSZ1Y{XjD`jmCO4Tp`JO`gq? zt*&jnZMWS)JA&N{dsh1s_PO@U4*Cv>4qqIl9m5>oIq^EdogO>WI@>#EJ1-tLIDY;3 z@Cn5e7f-agh`9v0ymjSuMY=w9V|DXzd*n{-?%A--gefG(MSjav5QX$UzX14#*-D z9m*Z`1kH{{pzARFm>^7}-(kOteqa1m{jdAaVokBxI0()K_ZZI!eoVb593(^#1_Cq# zk^_DO+6O)gVhchCy$==-z7#wfVi=MYN)-wXeQ|34sk5gB!gRv!o`#(EJpCeEIQ&BR z@EM~s_s=q%MV$R`PUc+VxrOr%=PNJlyKv^hV1!Xb{zaCH*o*CvYLRJCR8jD#kC)^x z-MX|H?G@b+BNLMpvwj(Rxgl0I_GTg=+53dm+ma5c%?L@ zYNr;a?MsVETTO?jcic6)TYgXUUQz~4MnJ|$rhR68mP%G`Hg9%J_C^jm=gWQT`*pd> zxw(0J^WyU<@(KCl4^BMzT%cd@xKOh2ZV`7;%tPQI;o(HFdvRNdX~~O6Dvt_F50u_1 z<0y+Rhm;4G&sO+We0l8jxbex+Cod}1D$A;*s&cCLS0_K^dKzEDP;;S%SQ}Ei_{{Iw zx92|32Vc0o=zi(=vZd}=-N#p^uim^ieEqUsr@r=$#+&N5s&Ah(s5Df(Q+ikaUh#d| z2gMI%AC$K}PHx6x7ZklfP5K&;X{@2N_q;G+(lU-@3 zsHmu^XsD@a=xM+|MmicAIz}c221W)3rd>?rYx}X4`1>6KzT=pgn0a>X;`!g4?D_^G zK?wnmLj85Jt0=J!0JlR^P*6gC`_dtuk_w@q1?a%Db4e$@k{YsoLCD9t?gA*l16?V> zCV}U-zqsc8=WL?Fwf%vWw!c z;S276Zo9A1Kks;g+-~U zBrkTE(OSmoL91VCe~9~#NJ}0M&DYGn^u97keoVAqzNyUMD#9%JyuWd086=s3l8L$W z9`InfWnO9Z(AE8^Ei{>mRrjextxcw~E{SX9dMb{7%e$l&eW{}6^=7l31*?kQ%h(^G z;lg)cMTs%g5_COxLCi znZYwn@8Yw{g&jC@>D%MKvj!Ka_%zkz4=Cmt)ZJ+Cvs9&d;5fn07l*%_8KNLLI&q|} zN51Gj5h!QK9Y1BeH=t+DtzijvvA0nIp8y8m+i9MN&mzzj7={;*L5Xy92)SgphLfKTX;OCGOh)Zongn3^hP-CzrHp7dhruMuH9?H$$9$fLf6wQqexct<{>A+A-+23T zc=53YvI5+x{U~8%b4*8QF2y{JVA+y1%AtwfX&1scx#Riu8>Gyiw@;>by-TD* zDHImJ;^`GBe16P`-6{4Am(nFg+vK%Z8>JTm>M>S>CUVoXZ`qA+)E;6x8lG1|1khSZ z&5ixDCHT0Crpnhm@5gV+j(EzJd}8Z~coMS|-JY72x@Ymjje{ShmS>v3Wm)$Wcl-B+ z+sU8AKmC}>hUrbe6{i3jJWcW7l8-Li+r4xsjf>eaj7(3HzY!!fDxLFoN#AkyM*)78 z%^@Ks2XUsamsF>@X`kkX#TM#4yf&ro7&n>wRISm`5})c*hpJf*oq1id)}IhhY|8R1 zsPt8=o3z)^M@Fkny6(Y8#vij!dS89$LODZ_f$;GVc6Qsq=&{9;@rC}3;>%&QT;YJ0 z9iso;5p~h(Y!};>%RV0x6jYm@iOb$h)y9>XpMi2fx;x-Z)*G1dZgDcT)8F^x<2l-7Su2KRP+7ZX)dW-8($}S_YCeMgb#eshQi1>o6kP@VSRJ1+=Quj>F`kUE=61f%0>^-(SwICTO_`OU=NU z01kv6`f@f|knUL>@V)=WC;s1Tm2)fkmxnX^OCK zOBSMSeMNTDL#JjYifmVJw64TTJKTBIe4gV{<2_H=gkTJBa1mxLalN0VYd$P>vj1J{ zV#txf0ge?F>Xxi%f9|peq12_hvC@Yfmfp3l@Fy=CDjk;RztQ62-VlKVkBNMHu$IP1 z_y;1~?a^f5?8x$J4R{ov|AK8q>xYP?ve>G~sUkz8>kpnQwou;fqODmNlH~PdNP}w` za5ffPm2f_vUY@dWsY>2VD%A1xBO7>zvzR>adU-DNahzR_qDoy*0g4EmKmGLeb&B^h zl!-aoXzeA}jISxEOA1$$fOxt@i#~^$oKx5M8p7q#wJe)!sNQ3d@xc+f+#ej!yCrf!pIw2|m#C zP!XoRlsNE{uWU5;G!H>yMM>0Nf|A1a$2B(pPJbO=_93L|c&PuB&>ijwiD;`E?s?Sj zWPt#G1#1cQ4|N&6tg@aG@G-dN2`eu7qxT)hpPuVk8!O-Y-DP#ulqzFHci+)-$!*Ih z?ZLY5xoeg~n-xEFS6{Y=Q8v4X32O-jeV6m}j@(V7H1{r1YifKJJod0RJRzLR=uz*H z+s&=xlT~q)nroNFk|MC3gb*6BMe zSs<+@;0YZPdHmV@qwnCA)ZRh=B1}wm!^)c6Ag*K;ez(2TKrF6FO-In0D(h@##Xh5` zv#YsZ_C&wFjCyGrSZ1+jPpa&p)7mlu2Dw*aBu*bc_#+_CtxMX*SMthR(Ggn1?aN;{D|KiG$f3H@P8jz5I%7t9LgT=H{fkUDRdA7imq-NHK{YIk+Z{Ic*{c zZF=Tr>$AT&o8eF3Gag% zg|c-bzUOFM3lmZew;K9TYI$Aa6-#pOosKn=VN>{9L7u0U@`1PGbcNCL^vc%EE$5eC z0n3&g1hdDjg#n1K%7U_0vSKs5@^81lN@E>UE7m=wqt3VnN#X0Qc2`!+bV56$ z=Jr$CU6%@ffBnI*_z2qc)}FxmABGt{p$GFvGHkKb_ ziVr^U_|`-B`6Gy7hD8mBO|eaN0VENqA_6%F5+W(H@*UrsoiaxAmnSFH(=IhzKQO~0HfjB{LAwaGkcA)&p*@FM#_V_Vw@ydj%k zQFPJz^2sR9+eHtbJ2Ki{K1pAG{wUoZ6=sIX>l)Ni>o_zuJ zRegW;)3OiKw+Nl9y+QTj6HS9}GR0>F0mWKPyAQ25C!&|!EMg8c8I~H#;;+UHFWK8v zyPVVK#;_*}x$sp}=CydQC)7AQ_==n5@OZ`{z%dox-pvl?;alxRjti4F_!Ihi#q!pMalka*D5UH1+*{Uo<^ zND4a71E1$uh)%Y2RCys;m@~k#I8H*=BXp8W8t77w$VNsx_-hav_(guE>%&^pd9*J^jS$t5)9`ojNxB{!{on!a!$P8v*quFEdW76<4E^UU^ z>UrH*eB>3&G*wq>uby(+=&J*l;FZgq) zGs}N7dhBUEwe7PY*-q58mT{yd*2?>s(bva%X^Fd zK=EX|{dYuCMLWFaDY!gPhIfG6UZux*&W2YHC>MvdaX%IrD=pY$5uNpa`Is*M@fbzV z`K(g0$T0THsvWQc4!W;b-}IC=>jq1Q*=F`uzIeuN&-D2`;o7bX$0LgB+Nv9pg|u2+ z5O)b?^2t)_k-4w)3f~M^`+j&)H6_q;9NCOO6|9sUiE>R~T1iOYc?1NU#SzMrmS%G& zO2IFQd#bKpVVIo3$n~lkGs9p#Y9)JS#6Dzj#mf^nKH-Rf^K$rCA}~0dhPpGGmmDio_X2w@OzWO;rLj=^ZPzVnZ_2r zoIJMH5{`&cnQJg&Kt4kEG{#JE1JsBB^uTgHBX5H`ef16V{5Mb5(tA=r3i{>t*vnl~ zk3VE>ex{4Lw4~?zpsBve;C`2e99`Xeai{sNuEE7c>avzzEIU3HFPH|8sO81odnl0_ z8QYSj>0y2L`fbUoMhaFs3&!fSOZiUU-o)n#w}erCPvP>v$H)Vxy4)0$6DaheG3$qP zX}Rf;WA48Tn1? zl+SSGADILXo%ZRkSRW2JIKY>%+kh9+qrf$36l3vxxSs#E8B@sjQ{O3&XMIA08jlTuqUwOU8dIGry=j!dMXRT{9nYhv_BD za6tP@M5sxIPSKlhQk>&{Nvq>mx`#UJU%g)cu_j)cM*N(=Xnvpqw%PDCuJD?~n%1$~ z>-j-%(|OODMbv3~iKRfaF0c$Owc@_3;?wj*r|)t_GK^hDlz!zX3%ho?ZBq5NUmk8> zGi(XilShe}CP|FvrQsu>VzPLG8GL z$&k$;jq!#~+(c$Q6=pY1^+%L?~e0czJOirfiJX;1qLU~bgw#cT2*q{i?;2{={jla!=;{C>5>K1bA;kW zPK$!y_)XbZ9uc{uTMu%*3~1IB2N%mEX{}9!uOcyJyUpLFKeZK?e;F&PK9CSNCHCQ3 z2PInMI}xz4o44_lPk*&io%RlR_w)r??3I82@ncVhdul5jX?doCl@&jBEHa#dqfSHX zKpQPke_VU=8~y;p?m+ja8%src{2^-chdvD2csxs}e}O(&8Y3jHnt#gdncL)p@!O7^ z2L=86EFb!c$U*{d8Z7#6oVf1`L)7OB9y1lEEZ8tqbTYg26DJnCrZB5)yWW5Cj z?Rwuo%$=#Tf+$~B?p`f&4m-s`Y|JCjCyXF^_qrK!(`;OPoE6X~o~w2AHhhTSlE{&x z=rR4n9Hn--_TvRrZ;OoxB~!!&b)M54tGVN02-z=Q&kyu6#HvIq)Rc=p{Cwj$bEx== zrNyl=!0@E(K(`AM;^Cli_A~!)xlF?&&o9=X^)5sZyw$75KNj1hAj6>JV{>LNHQ8h4 zxcrQ32|sMWpEb3giU!z;Ku`Zs#@f-w7s+zDBI9?pUcR>_OpmU>HTH!w?lxJ}GMG5Z z!i}`)IYH%T{`AgCAM17NYNvS)-Jn)Rd1^}Q`iT2=HK#VGz#osrszp{BS^`k_uSe)L z`#ByrmM2=C3D0{R*7p`|gvn|3>i#||@I5H_gg2iwhO5B8NR4;&^(yC&wU9y%$6(pA zHLGQr;i_Sed7F`7?!pbNH}L8YXQ~)`Vp5w6?7R}{Z+~o{^aq2;0vRT_&o z%n$6&8B{t-(cF|Z*dBM&=R=HxG9H|yos+u!av^RUIY zwylZe1nYq*{O zv1z1p!`f5-;#c|X%5P|oD%*{L@9oKm7Ek^xh5Nb%nY<|mH(5@GhBIsMcuGAuR%cz9 z>8jC{&-RAjcJswcD}v*3n;NZa=O`nVqC*^geegd9oer44!b|e3PBr9(N*F92O&#Me z9p9&E1%)2wmraqJ8J;Qd`Sz%`AfLubkmk0I?RA-?6a}ZSllG`T}z3z?h8G% zh1%mV&V@t`ftFyCgew0?A>l8H@)eT;)A@t1%;)zxWQAj&>cuwt)XBeSGsX;@8>4iEoBJIG$wfo9v0lrqKdLt1t+7lY|BeYR!FCS3`7}q zY@~R1yJM&(<6y|u2ZM`29&V#w8X~$5(LOIv*EG>}U+gY1r(+S&5v1xWWCO69= z&d+3qEU1(DTJY_GYHU`^l1Q9O3ci?s?nMssbS>BF@oOUT0QD2TCY6yVy`mI+!h(>Y zvqNt%*_GXoB97Qa!NV-R-%#OMj-c;Xs1a(o%?=qTaQHz4Bxb5Kc~>#5dp4}5QgqKg z%X+pK85(RCs^yBkwZ@(8Gns;*d&cK=Gnh*JQSu{Mk$n||<$bER#&4v!X-=Z4He3(! zHpNS=avehiE;%TlsLV2kQeUAlbf9T}JZx2fZfKm|e{4vLQP`zwCHdGn8`<~vyI3-> zD4jsJ-yAp=({fQH)i&Y5g{pmqx3j7iG7&`JuIhu14Yz%5+EJr*N7z2acOG92yVc?` zy*Q)TyGx|_L5yd?SE|s%9rf{N6_nj+SqQ?^X8PR-y9Sx+4)+srs{xlK{JTbvVrihd z6;#*_f}zK;K>nqtc_HQ|(66s;>Lk2ui-hu2>P@y%Dj%*O_9ddc~k z^xXT;JW>8Ld9`mX2lDp2ah$qWUmb_{ zrD}M1G$VGCqqie8-$Lq9zJpovhiCM=iamk3o(}AE!K#n==%pt0_rA|N818pG`%#24 zsYMy-+N&1j6wlAPu^yl_9JHxF_CA2^C?Uf8$)?fLXyM>T8`c9NQR|{sdIvWMMSb=T z5$~G!9aweKuCEJZz2-E0JrYsD7vN}12q?tGC`q991|Cm7X&ZnmJ9>FoTkhd)K_qkR z)XU!2*CW-Bq!khYd3H^8BV14*szvg3mSv|Azxd3{RL|jTranVY9gcwpKSE%5En5 zf6+vpR9gyqGrqVw|G~t^ec)*2y0$;}(h1(t9=tnG)2DNGftVEw}UJyLf zDfjz>Q)Lsrm-ZeJzOrkxjW59OTW+!6H@`QrBNL5p?<%*jbz{11Y)e7{ye~DjB;>_U#$h;~_-<0uh+w7Xy72m5Jrq8~rGRu-Sh_Joiv(vO9#8l5l)B|GHwlw6#%ru0orVd;D{;=A$pzk#D|LnZF&F&VopP5Rh7X zGCTcr?a;Uuv1MfDG8OVnWV|{dFK+jFY0=Uy+GA2$`FCvlc&v-Ie4s* z1)<7p&^}G)s}SYKTnx0MdQ5Pvs*<-6fqo`f>0X)odKEH|^k`wN}w)s8JG zx-qE~+2`-sqAZ@?OMkOinA==w&p7I}uiM~m%t*o~Dt;mmwO;zAPFer$t&l?#DUM3( z?GnXo6)$n6kU2T$TiGEq*6V3zPWQs4u<5T0Pz@|Mil^^2B~Zq0JooK8Cz$SCWIjR{{@5^d*#pn0FZe+(<!eo1Ff{soBq$& zNZWZ46?KKyb%CUNQ$B`fb6cBhn?7d~vNctz!s#Bz6DYlWI&}m;m>Y2p<0LO>y zyWL2s8vDN5(lf0eM*3){%fnObBkEdkt3mNG{Ww(BKxJ|p-YTH1r00RAiyP?o_kDFb zPb_4U*azEDK%Pg-_&;4Qu*d(^_2d#*E!Dmd)U<|+%~^Cl0yQn! z!(Q8V(yE4ym4jb(t_k2f#Y_zuk=2R|72=oFAH1i9nx0`0Iddh;0#ZMTd5Y8Psn(4o z4C=vwE2@#D_XFWHNpMgmR1#ga+_xUyVy~K-k@Uw>iyn0n5KNjc5O3ecw#HZGd&cVu z#YW-*(!*b-^)>~M8!8oy65_;O`j57v!&1FLk(25kvx$bI%Ed`8#k3+cH+OFPd?JXf z#mr=4zmp>(w0NopC_fUY76<66#HhZg>oaC$&hi3?tYeKh5Uap@4M*j2cvQ@}88Q)Y zQ9c9NZXo+PRs1E?eG%#flO7(KGPdZ``BLkugY#dQzaP6(MlLTJ$rD7tGB_ZEz=KK> zmDhbc6fR{*!GP$&wYSrHc&XC2E)*#?#ba;#WSunmZ_B!(#*(tQM*470DZzmpM}0}@ z?w``;bG-+FZ@3%$J=C!DSuo>MxPtMW0Xh-%)4;=GO0p`3cZ}E-RsarQ=|L=g8`D`b zGL!{_U*Z6e4{o%qx8YLbr21ph@Z%MhXvdgsaba8fy&sy#+wc5GQR!=JRR9YM0bZQ= zdDrCc0s3;IPZ|-%{{W@v5rsOF-cQwPdR8^zbKCnVDioOA{)(top*A{j)|y3vT`$}Z z)kYD7b+JE$R(%tHIe#(TYgAx=|JLG2R6T+48?=1NH$6G4c2|+FRP-{k#L-9t#OriF z6_@`2gZ;DuTUHqgja9T=bR(9uu-PD}+B+_n=SvS;MKL<1v={BM_I%W=61GBLBXMiF zxgCeJYfGbp%5=TRwT-=+YAQ@Hs2OJ2v9(s?w5j9tRm=EJ0_vV&Rv@dJt8(G>Rd0eT zHarfJsuCpdl_TN>d#dMGO9^!ee78%I0|Cd5yl+?Q=Tp^Z9zz&O7Tc8gNxviARWFqF zJT4eHtt@fM9aDj~%}&O0t~e6Qmk=jg0l28tdv8%i&u}GI8_prjRf>v$CjCd_=ndRac#5o>g8w z3p)m1%g-A|<5h8}V0nvCQ!0Pc{{WiC%1I6^l0|WI8WsSJH*prJ!06c7auI0-)c0De z5Z@vgvLj%4o+8y!61SJ>{5NyC0qhpx>^+pev0rM&H$%i%-Nm>vo_w{Xc!&3?n6a^= zj0o3a5vb?%(UKS0;nVTld2z1LbR_GiWf*^B*dCh2EV}3ptDzr7ALc*Nap~BRAXQZW z4FLkT=ayY`v0^QNvEf@t%c`|tZSogF4wbtjC8--yc+eULQOAu2^P7We0a9iL+9E*r53+(3I{RK>s7~)NirtKg$wZI45bTm*D!V(+>D92JR zHiK0(sZ6P7bq^ARxU(@JSg&W^tL9INn^DdD)mL{1VcZvp97(yYzC0l05y>EFtyXs=GP|3k)9dg3o8b-0_wfhgZ zm2EoMBE!-8R!>Pp&c=nO$|C#7-LM^PwD9*-_0^$d>HTZe8F{c7pko%_<~Gu!Z)oqV z{EUly8Cb*00vVXE!*OCgwRn`K9FZ9b3hBH508sc(vWx>}WJMMCkgNs4ULQpm5l=Um zOQVJ#dei7>-Z)bv$4;A3uAP-os>{>Z z=x@iew&pXnJ%@L7QN*4Vw=tcoRx@pkC?>+%8r23PHvsn&NNdCbZ9vwC8s_xVtQY_k zDpKyYZrx}-1=jxnL8eX8Gh^LqR*|&E48Gt#y3W#L@`H8z_|)k?|IpylIc5rqcPTcv zXD~af%aQ5pc;`^Tf$%NE^>9_Amej5u0tv5@+Zc=H3_ zMDiM$1C6VwOFc$BM?kYkZZZo>8sdNvBWBV%iuKYMZTPzr`e-pYHs0<@}2t#ETX(%><1WG#C8e zP`23{{{WW&KhoX)f0@4JUbC?&)4Hohh|jVNM;F7vQK*jg^B?z{;J=Pm@q9HMaw1nD zu_w0ZR0ZPX_=z3$o+?l>q`*vh5kONVuA=7TaN|#ydf%n>mXE_YF|oK0d#?=*RfMqO z^^_{4O?xewK^ph_MNSe=8>s?8X`0LNY5=!=JO&%o5X4)^{{YBs50*){xYD*mlDwTt zY^_zwF#iCy;9OGCf2p2!CbN{RW8!to<=86|ZC1Yi@Z*Rb<3*VbATYWMyf|`s#Gxq$is(w%GX&_CF}^ zttW(hkAv%Mr^*ns#_Yp+f;~uI-r-Mtor`}@>v&lHPRgyvQy{@lrXR*Zrmop=PgsCFni`a@# zFwE|ONwkB+yL>13PXkQei+-*K#XVf&BMuh(WHEuYAP;6vj z+>M&AXMHPG9*YE@5^(^Hd+B|T5_plkWCv10@dsW%v{L&~kjDQ2C;DKrk##4HA5+$2 z>oUgSw%M=4k@p&SksqneJ8lZ0Nwkr$xb8ix{4`oIBZ)l0Y`U$!0(X5KqN*mHff4jW zZhl$ef5LfHVQNzb83Dg8e63YEO{-1=8vsuMTXPxIyg5@D;92z>D?4_zlNp0G`dBFpi<{*uNqEa2Hg@6WFdJAW3O)vD?wi%R zlpthvIp>_a10E8wypu8TFNcEC=HHrqN%&f^dj9}W#m0(O1I%ah8DR}9Xv1D#H+U7V z+rvMvmFLrc+ygl^W%rr%XQdX*hx_)Zz#mGs!l#7pbQ#=Ex zvVccB)yPPYgYLaKK>L@< z-}4O`=K6lF>W|5Trr$K(q9;pSj=(|tYJic+DyakqMz{fV*N_8=rDmHFZdk&p-dOWn zYuUJWbEgWACN@{)WAf9>iL{Q##LP~n%zhI~@(JW3-Z)kFECiWk;ylLCkfT;(ZLzQ6 z{p#DFD5YGh#t~e`ft-XkJ;j*(&fhWr04-Lh4fMFcK*CZ5_Msqf;>S+qsq5ua;jU)g z$fE2N0^h~m?Ee6&jncrLCEF&(Cc{>>h`S5ki9opL$(-s~vs?mn3N$yTYH@1BG0hOL z)L0(vD>`P^8*R#s2Oi2mWk-)`3NO!2R340X?cwoh?w~dhu^OI#Md;AkdRTkidk?;W zrW2zAbkLD+Xs3fu!PAUtHL(?PCeyINw~ekoZ`)N8lf;!(u167L`WmT9q@u_w2Hh>f zt5bmWexa1TJ;pd~$4NHm!B0CHud!mH*;zs-jz0#?T-+))?ko6Q^Z9Bo!$^OobuOB? zEhkW86j8R{E)#9-_nQ9z4XJZvo<-E}PV2bQV~BCk#}cS@^Me)u>P&~3*OgTEXTg0E zG4;hsRpOEzj-z~=f})`3V`gT*1*i8fs#m1-E%K9>I!})w-2{r4TX1WG;u?HZzW)H7 zJHlXfX*xbsdDxLWhj0>kYyyCO6T{)B*LmeLo$*~Gr*#Z|tB?7JmA*~Hj}6G+8-rV& zZ_l!IJTU8=DUyXH_LKJ2R2G)ybp17@p|lAw`zk0K^7CJRy59Pb|I_)hXL9TtQ^?mZ zG2cgqNTiHAAXi(E$L|h0_taTs8gB+-*8xI}L0(ooKRr`$QXG3723DG?rs*Ue3d60;F$r?C6!1QD}RWw=Og!g zKMhw?;JLm@I2Id{&Ikz|_!4d|YT|FgTce^k<^c(+HMa5KS3%2-XWz=BNt81&J_aP) zQLenrM5gkxd2E5Vn+5<2XbsPczp|*F2sZ`VrHLRfZoEkxh3L{u^gNi5x{)3eb4G*B)vdU66?qHSwcy1s!$M zl|QtlmK^Axl!gb#iaAe^NCO}SQ+^`i$Avkc1$8YJFISNn@FhuO1bJ|l*y^W>l5{rj z_?DcOd=}5j>FEBraiDBGwEVbMvm40BIuMQQvJVK8_DHl`o``?zwQ*Aa@Q`Dld^VOgtVN{HN{V!kpx2 zaow6k04wQoEvK;dQ=m+3-c*gdd||ALKo{*Dzq?DXkqzOL0n=bx8=yM)`2}7Vxzx2P z4$Zx=)JGFAh4!O4V5UF_@Tt|#;!QHtWRb@dW;R>xC5tw`A;-cr)9%yLAuKW~w8fYc z>i)r86N)>7nPY?rh0s!WA@bKG}I!59161M z`M^d`j9TE3Z~hb&O|~dt3Tk_K)Q7y8A_Blzeebr74TJb>PNdjS1{ncv4-v$6Yigh( zOBr7Y)LT>EOq$CgY1*elMu)ujAb79c`zh&Qk-gh!B(dS!dNdl72Oz5NWnISG*wLc| zYzZ;qEh{XG*r#7@Keh=OhZ$lpgmZ{opDAs*P4!~d=Tmz8szi~WCIhlfk~DrLvD$VY zyMGR}{?xIwC36ep#0t$2xEBk&0r3?YZvOyvR*W^#8>h(fN>s#L1|`Ro*0*Ka{6Wy$^wyY3-~k(?nC{B*d_qD5ZQXypS@r#y ztmc_w1%X@hQ@Yr`es`7QS=CBzByCGD-EDO@@Slpav&&5^(-)1J_a#VegmE3Z zbD~1Dcx8z~vW4IQdtX7u!v6s3qJwcfOnfV|b&9bTA5f{LmFeCF=d{@0;a$Cg+U=9@`pN9-9Ur1ECJYi&~xnQuO?_x6h_i~~JCC^aNH<|#JJ0aleNjpFz#eJffO-F?gf)kPAX1wn9x|JGiBY`dcnsU-j z^h{xm{L;lSr~aHI_(3N{7vWk@{{RtPhaHP!#Le*(QhlUXWN}d`ho}!wAOF_*p{?OO zE^Ne`*k7`_NMi5%G+q_`4iz{#q=*y$br8!!YqY11a^*mwXF6i1L!`x z7n>roJO_fPvtP7Puu9vfdMB*Tt7T=79uks8D`CZ!FGyo!Lm10;X~w5+fc+IVnrOlTkgK{=Pq8w|;q=Bma+6=x9PE6R@IeBc0)0tQ< zGU?*Bja3ql^rusN?1$+=O0F|+Eo}=P&vg{j$k`F($Bc2;WViB=>JGjy^3iC)vGH*~ z<}x1%NF~5Le5-&o)0OIN@_644X!54Wg`$tlhD*DG8)*l`5r@%FMlEYGNY*&iIF2*s zsp7g^Ui@m1f2T3>)m)gsAR!=Gu0K^@De2XESqG=QW06$-^1U+x&F?&X?hn@i*K0|BK_-L!}ZeG5A@6osn3#$z4f~=Hy!n# zUn+^V^zTq1NQkp2w(bf~?uv@2N?%X)U(5p}8=1}bST(-Iry$g-`fo8d4ZDMCcM?hc zJEO)MIe>75@U7;O48X{U!y{v^?*Pd`ah6@vKfVrdvF(6LZ2 zo-Ul!fI;_CF+bwERIlZXa8y{_>-N$1EET#(MU0YRKpNZlYid5!VY5CKG-`2SP(JV! zS}?4JT!}3k-PqlQNxhHMmm4Lb$kS&5Psl$HRfNGL#N7O#QGlySHdJ@t$A@U5g7ISo zn`F|LBY@*-kjygmM$8KlQmfm>s2a+fGC2W~AkX+s0pT_WQ-s5of+D9Aa+qE(DV`=a1$k42Q1JALv%0gW+}>FP z#@qp?XvMFmV&SGhY}ZpPDnPdZZbFurvJ4l>Y34f~ViqV{iA!;{G9Ugk5?+^*T`v>F z!BBnSs3M%91JSXbEQFDm#gQoT%Ui;G%^;h>X_5gP&XVN+05cvXwXJ`ifP11Wcvun* zyU^brVBEuT+9^Y&;OS7|=Sw~$Eear^8O0obznF0AFTL#4re~4rTz(cumm8)X@G^C1H4a@=!o6Gd4P$Ij*gy= ziJqQ`jgf(ojf<6uiIt0ogM*8MgNKcYd~H9rGJn5Am>C(FS(taRuuc zim?4p7!aQWY;*tu$e@G>0TgTyN;U|w7i=U0u@<5ON=XeNU!cIGixPlPP*Ky+($O<8 zf?fYtBm|(K+(GUFAXE@a3MvX}S{gcPN+tym$wo=FpIwex?}#UdP{0{^8qUN!5A}sb zxNKi|DJWvkHX6{1CfUsn%t3KV+?gfk#0;<4dmFvHi&x&=k!7N#p;jV` zuwRay3X~u~h=W@GOd=;qf)}=pvkD?!1K6`k26kLfoZ_4)4M_l6F{PJHpb&R=dmA|@ z<6&Qaitw)`5dS3tu?t|LB-hCX=l~;AeEumLwvGylQra|*ZSEEHHFJo)^e5salX@Hd zdfF<*tPi!E%CzTe3}R+13`_@jt1EJ+FUZavYJHabA)L2_wka&(Yj*tW_~ZPis{?wC zirYB&4n+vD-ipSQCf z4;xy0$B=Uo_`o)Wkgf@$MV z>tU*zjXZiNX+qauux+rics?S92o&DKGAuIhdAQ4jzI99;^P_ou>9JFbTzae+bxT!y z@#;r(S21SkP#L^p-DATgSm6p(Cy1aNcK-W^!l0&2*Y9_Xq@KLaK!^0!Uf|yJ-z*VL zc#?~NP>9jW>V&Nv)(Lexh1j@}Hu!d7W@YBHq?FLRE>;=ECm9h(@fzzbhn_tPcP~By zTdEf=+od?TQm-&BqjbvnOC28-S}NdcL{QM8P0G=@A+{%R=N^_A-wk@&zg(-e^5alP zRP30R^<ruioZ=Vm2c+Dz zlAe8m>pa>w4TRnLq?4a`QTO7LC7lAH8$_Q2NoMCEBt@ zJ)b28$6kIase4@Om!sHrbc!o^ikCyD?*`vN7JuZ!zR+bN@b&=jxrEbuMV~dVYMGnp z$gB?o`tHQW(aN_mZ#M08U5-o%h!(d@5=K!cqu$b%vVOEb_4?YU9K4Xp>W1iE(;PF( zFqO(M*whah+i~oBb=~HU-8b<|s;-~)X!U6IpK8!(@pko^`ZW(_$G^w+w$$O`uml_G zj}ImR$>zoDnO+YUdsFNr^In%bT0F_(SEkk!xUa<_x0$MTjgP*uNJD&6rltF-kct1D zJs6|r%B5zrhocB*ksIkRLfqTYfjB9^i6b^h4f7nL7#n`U7GO=GtSP~<*w7|FwdtcJn<>o>E2?e zn3C~31bmszzjuxq(bc>e+VpOm&}0+`y?u;YGA^*Rl(Fa}5xBchqsGU#drx`jK=vm> z%c<;^fVqc-i>Vdi7(f5n?mQxpbvJt%HZ+k7lY+NCA9#qQ4!D+)m@YbR)A(HNJ0($hC>B$t1Xf0Eqw2~ z@MiHm5sU3%ff_e1DWxLjVG^{_E)D%>aU zLN!XnUzF#~`OD+scF$G%4V6lVk3c06bvd-L?rU)x9Z&fm!({@x8f7)F^DXMNI{9d! zqFiT=lnZ>^Me|~#)rx(lFqb+r9n<;Q9+m`9~& zGkM$98Lo%(z_~ZpbU&XDog|zZzZ^mY+;1h^Egq}ol10P3pOoBa?Lv>31wAGb*eA}v%_CLOf`W|F+8m*e~`rT z*?UQD?yyK#Y-_tis@_O9?Y)VPt`0ZfE~>SlaDnV^F2~I$Hjled^eC%0#CFZrsmgr| z<~v?-@>Kk%$wR#Y^0(%?snEf|1QpvG2-Gg46XYQ;dUw$8GxYlE!WfEbB4ZnP{ z;t!2Y2W5VU~fK=8=rZ-(@0u9vBdl2puYt#-L3%AE) zY9!u86jph?AK!5H?p?B&pYXg)d2Ew+Zo&FHIO*D)G4=oU>|N?^w+Ttat6S~tZL1NF z>!LA@u1adlH7!09drHrK|EhBH@{IY|H_Xh2hLcH4Wq9%t#Y(qVqUTMu`5bBQK;3+M zLexK|D`rb6GF&25xuvALNY=p9?|R(OX^+cqWkI}cPc_vVw9K!POGUWVD<6HI{z0eB z?k?Lk?zrr(aa!c#Q>&LQ*c_jZOZ3r(SqH}Fz35tM-@Lo*l)a%`R;wq-uGyZ7ce{R# zP#C}`u{Vk`ck$TMJvpw;U7S4+vBU`?`0P_DfSw z2QMic*A?#@wHkR?^F#dmA?x#wNxjeC5UeGG%AZh|v-5tQq4-)68oAIOL)kOZ>mgM2 zioWB=CL!Gf7omhxI=#G_pz-F*m9k8wPPdMQ3220QI1zZV-o9$&bGvOwrRmYa`Ed4` zEaVc2~OHR13956`Y;Cd~&Uj*ya_rQjy39b9?n(u8K7I;BHdlvarS|bf{cBER7UN~L@ zCEdA^Z6~~&YiT#{3-Na|_BAJ5H1PNRz28M|nyt5rjkL5I9DiKpRzGWCc%uKlP2Fmk zf!+{X?$Wg$&69Q>As*5rVepr4c0Z{*x<9un*tJ(RAbA@bh!yRRsKz1J$6Gay(e zg5#Hmk(E7D&K=USYJqhdq6o@Khp?M&7SG;^Tu=;qWgK;?w@hKhEq7IebMDSvwnqhx z>S6RpP9{(6XQa&c7*SQuec?}Cb@zDl0<^okb2IT+Ns^vI?k#LLe3!QnDg+tNp%~Yd z+4ciIt8c_j1c)v3<UNrDo;&s0#M< zg*S($`S`j*)oZsCuTEkz6BcrLz(q94tUum(_HC!--f|=cOmraVGml*BVWGk6Q`omOjGA(FLv9 z0cIECLeBj_Z#Hq~T3sk_qpdI#n3Q;TQ0lXy$(;QINhM9kDIWaHr}&y%-a%UU^_(#uF?G-q3TBp@N zN+5eGYi%QbeGO?jMl&ooqmy$V1&vPKx6sqDQ8T)s=%bz@&3U@f8{N}+4j3EXm|16* zmU6J{nl9^PT9oVAS8(nfL>B0^E8$&Ubqsr2^XBM=aK6S=1hoHIiiY+}_oHW8A58V% z?Ord=dGg-V9Fh0>{F9>ABD`A0nsGa8@2WNtc)wimqg|m1%5P8z*N)3FrCK|&p{Rm+ zT5}`%7L!}rNOJX=`m-kr9z^%vagvjO&cVYkEkB8qzVi6}DIzc`9LjZSB6;dUIa4(z^T&^Y<{o{ov26Mt2#{^Izv*oJ$m0WQ>{V~@5ku;hSRHi zyOYuBp5FB%;RhFT1QyCSgQg?eK+aMYR!0}AO!DdlQi%XV^pVhyX=X2SBK)!U^EY%> z_qn$^a-DgANPS1_1c*~&6TkdM3evKGs=k>el_=Sfv5S2F?MB9_lL5ovorEWOasYtQ z9}7q0tqFL40v^ng99@774sYj$!63;LW@tPNh9;m$2vVB#G(ciWc~){+J9sdOLLZJN zl_8{4p5nv;|us<`lzBtRD83QcZ;8zBX|CO=uLgId6tbFl-KQl%s zq~XsDsK;Nd^`QvgZAENp39vQMHvpxfB#C6}Ehy-XA$aLxzL2uy%LI%52bezcAL;b5 z-u4b?ys@w?lGL|9FMXu9;6H=g;*fYU+$I>QYx|R&ArJ<|W3Yyvcu!Ib$U@rq+TeDq zf|Mo^gHMv!fCbEFi_PDvwFmv1|EyX&C~^yKgN5q4kT{U9T~I8>-^mXKb}ne4aJ27^ z`dCN^TTmu_3?7d`Au(v*?IKJ&7?KFT!I^j9ws2p+-w}*E2vD}4^rW#uIkhFjw$Br? z^CXWWF*1&2yHfC>a~jyPH)b-7Jzs4?rCLl>u3~6Z4 zd3oYszfCdJfCGR7=MF5016%EdA)vi+e;Zg(yc~!l^$$R@#-Cjd=Kp3gfc`JjdPAbO z{b@N6Nia);L&BjjoFmeLWc!f6)I&|ifnavIvVOUQ*%o_v%_96Z|vogaLF$qG#n4x?H!N>n~`zZ-n-HA1qwqV?P0-qGn}ctrNzz+L$?F|3o*SP1{HGd*CeMrt#n(j+_D=v;fC{pb!KMz)*=Yi6PlD#F_yn0UB-;#-HTu zCwWLo6_GAl@NX*(-gmsa+b{?zPa^v5BGc}`sdf_o>Pad{;_&D1j}HFW!2g>z@cZO( z70mpK*s$1pVtrwFFx*QD;gMn`IPf|#8J8D9n zp-5rR-}9u<=gTRX*y%@LK3igC@?03Cq+|1$j-$ILwUix%?u2{ z7_tu>3EPP>GLSOH7%U15xom~M8H|5INQDpE7@2?%CN##<6OV_H0|YyT!3a3U7YoDT zz$Wd$m_os*fj8JdBpe^|m$uWpk}zgy9}LKk6r7@m`gx+!FyvNeLFSp)h=0_?X=;rtug1Pg|(e#5i>4Q~tc{tf&0(sp>T zGq&-hUj40dGnA(<%*GS%_m_APrvnZ~#^~XGRkWizCQ?)XZ3DT%I=J+ZL7pKvJ#TM4 zBwi1X$HKh`cygzbBw+ptF#w~H+oF-Im&pK$!NK&v(3Yp~&px3uLPIg$;5gVO1nB^F zl2zGx27=IkD+>ci%VL1>5BY0DNJ8u}B>02zLQpp(UZjwyKFkl~__O;-MBJu$6mkb| zfI<0#HiyRV1Tbl~C8F4V^8AMgV*@C1CpuN(5x0Dz4o z6{%KmlzYMdVFc`06iIW;zep`Tp*x+$@(b!V04JlJF8DRi?IQQ0rKi6I2DBis z$ack~?QBdtI8sD(8xDd2Rx>mdN$@6dLy?SVr+W5nxGM|`PKsDCp#2j?V}|wyNl8q$ z=QBnNFy3v1CJhJNHg8h*Gj8>p^G-0BR3G~h0tQbSOg~54Prht+{+>`67H5R=$A=hz zq2wK2q;fk*Fgy$gXJ5Q0a=RkZOvk!ik`xRE2hZ;)COh!u0mE(0{=c=G+1}6z?v3{| zh5fdUgk)^A;3)sw1V96Jg@K*jj{ZqAkp-hX*YPGjcXF-pSK|d1p(To%%VpvUV5(7D^rr4z^}HI7af!ZG!>l zkl){#EIU&D1w-m1RwpbR53_`$;V4h!b|vIFh71P*_82?3Hw@~D{mZbU1yxNl%AXn~ z{W4IZ!yoAn-{+{>(4Tbv;93rH<=#*k7!pPv(x6`DNX;iR_21c_`m@Mi zSpP(mJ7cF1s9Hawz1q?X{z-Y4WANw@d%{)KdV2KbM}-cWTP@>i*r5IFjLVKOKD zVZK6wKl?zPJea|nLGcuRhyVJ-)e^#iOAH0gj0}|J)RdK_*J|o1t-dPc#%} zW~e0uHdY!A_f}UhRy2?`R5dVElh-#=l#|m_)>kxEF)}t%l~XZPHqhTGYlwjoNVc<6 z)|*sT?SEEwYpIhb^1tFi8p$MMQ#XZ!b0Rimd#u^P0=82~xFFfS)IFi3H=LFbl+@=i zZ=r24_@4U5R{uxIw^#;&D-{2&A#9Q1Fh2MoPb^H&7u3*yvNM@~B{$A9@IOec_n-9r zucRj1q^#VIP5u{BZ!44Z+r<|KPQ5Zh|BH4Q{jb`gB>VqQJD?aeX^GjdKJoX&f*-ng z@I9v`YhKiXFikJ-+U2kgI84frJuey9C3&EFaRtWEI0WBs44=Rc10e``Je z2U?dQz2de8vdrJd_*ULi={!{}1d5d?l!R|5tO^R=rzGIRDEzjARew z#{Twx-vNOi4W#7HuNCc`uZlmfj=7fqk%sf_@jaUhcxiZ-vL8|U)n*SH-q?< z$r=2K=Wb(bZER*?L=H1EnSwqx3WmNK;w6x>1l51(vMe=HblfY^Za3f`dS0!*I= z)5st^DM}5Q1=E~f2vVAYY>Cko~XI;6?FCPeTss2`Y?1>sCs zds_oAtqD4!n0$Xrd;ONigJBnt79cs{$=fyr4?qtJf({#1K~q=|5{z(3*?>+cPpmiS zA_6_#=nw$dYBQ-6!2SvSCyAgV zXl0=L8tiV+jZN|_2*S~SajX2s;lC{Q8wXNy%dWu$N7g35=`IcI`osxPPQ3@HI2ZxS zV`(4;^0VJ8=pDhOB>>d}e?|->bB!F289Gtv!3^1^@7lab9fm5O5MW16%-Nfy=-( z;1-Y$WC3|V5l{kD0M)Qd60*YCy*DAw~$6iCu9IJ4*3pQ zrJ$r>qTr<1MJ-%_s#{d|sLH8oshX(Lca|=hE}!lhT@&3X-5UKadQp0H zdTV-b`VjgA`YifN`cL%3^lJ=k4B`x04E78s7|t-`W3&229782u$%zxlGTQI+^B~nVH3yb(vk6@yzkedCaev`Q zaIi?Tn6Y@XM6jf=l(RIkOtaFnin8jly0M0`CbK?b{lxlh7yT}=T?V^6cb(dmvg^sN z_Fao?9BguI)@&%YOKkaUZ`sD!Y1zftjo7`}&$HiUf5kq?LCGP^VZZ_9ILDF2@tR|p zlZI2A(~R>3XDnwS=SR+2E_N9b9 zJ`Fx7UkqOfU)NrWy%KwE_lE7gyZ8Ox#eMwyO!oQjySeYxzHj{8{JQ)n_^T`6`b9_cfaX=!u~t^Kki=>5*Kn1IxAEx)GN#&tSamy zd{y|R@T`b{h^5F$kpht}QF>8TQ9se^qV=N7V&Y;>Vo_ohVq@Yw;%4HZ;sxS82bd4& z90)j&d7$MW%|X?Jh=ZvIKTA+ZC`tH9q)2=^L~%&@5d2W;p+-q+Ni|8dWTs@h6qA&m zRG?IW)POX%w1xCp=?dv-84(#*nM9d-nN3+GS)^>1Y_}Y{++n%1a+Px5L}(m-hBYm8}%Yr-_MGzYbWv^=%$XnoNZ)b`NM(Eg&c zU&m7?Q|GI$u&%dmj_#=5LA?`t_w}arW%aT8W%|no>IUHk&kZRJO$=iUKN_(cIT+nC z>N6HH_BAdro;6W22{U8R(zf1?!_M6<-)_lX z-#)><+u?u%!QrJNtK%`pJjZ1xL#HICFU~T~;m+?|cwOKwk6me99bI!=7mpepy>@i? zn9{Ke$J&mI9}hnM){WN<>Gsr})!ozmkq5PhlSjVCx~H{grstxUnb&QvS*QW@I&{)o z*ZZpXI7|zc1RM3y@=5X;^VRab;ydoAFx1)n=%dLsSAGQt{>iy$JMki{rE zln3ewnjMWm*JAiF!I(z>L;e^1zXYfUTnm`RnqzZt5ZrOxV>~DLG4-BsfDlO-2-FTt z3H%Y{81yKZEf^jAK13qqV#sKyacFiJRTwnv<;ndg&zu|x*9*UM3UbQp)XNCri1QJ{ zr%g`ZJHv1WapuEW*|SM!7tT4It31E&{OR+9ktUG^7g#P}FSJK#M5RYlMZ=>%UR1bv z^WtWVcT7X9Y;1Du`X%V4hB&#n8*#*VpZJdn$_Z(gX)dELw*s4<;UZJZvj5FM0V$^-)o&XzA@T zj1IeD44J z+Y8?pgD>4*cE56d)lz$;_Ty{w*Kg{K>t5CC)z`eydQ<&Y{q2(m)rN|9%J0hGE4?rK zp!A{aqcV67lgg*ZpVdBBHflCLYtm_Y(QMdU*J9T4uGOlwvCW~aqy1R>mk#fau});? zTvuS%TKDN5>YkY1UA@VDd;2o`5B3*-QU3CDz;NLGSG%v>gV4drA>7dV@c9wuk>pW< z(Y!JFvFdT-@y`=SCx$2eC)d7Rm|~l{J$+!hbVhgP{jBrs@EmrI`2EuSp84Db#f6uP zHj8~rsHL^#m>+w7+*?sysb6(k9bF4rr(M6Xad4w@(|ogshytVazfN`~eG6=z>`FsL zMMX_TLrqOXPXqoj($Ub+F)}eQFfuSO?P4Nd+mEfx-|rCc9mmYX%(H73&;RCR*EbMJ zN(guq>aUYs#fY^4xE-2;f)euEmk#NaR0stvKnI?kOFHqD)R659LO#}Y7eE0X=t>DT z2|Ujg00FU2&GXYD~H{9lN7Uiar z{J3RCTUnR;t^R5Kp&mmbE%`h&U$go$`pO^$u`&Jy=CXsU2#b_+0j8a0kQ4?=Cg##i z;Qn&Uyz=V7EBn)0XtI>5?oo-^noVV2l+elZQX2i1e^DdmV#Tw%&1MHHR#k&naX-Q& z>@OkcRpRI#Qe3n>p-kmG$1Px73MjBC2Ha(?w?;W~DP1TROf#ZwHM9A$+lH0-RG0Cd zNMKd_ks+0v;NH9eUy3vlp9@+Ukph{mx-pk6N2ucpE44QzFekFo=Ci&pAFbaoUz;*! z2G2CTgU>D(cH+pRZ%_En8d9k0+w`npKq=p-_IiWAwK~mx=Lvqkc>JBLP(`WH3AMH! zh2ncepqwFZ{G|Qfz@9a;r|@qp(A|fE@%LMH-_{dst{uag7hQcjba^hLN%bQg|726a zIobzVRE|_`m$Z<~!l9?+m+aHaKNvECh+=K5T0n1Iz&qvNb6zS=as;W?OOr~dgcDy3|Yu~`jIHqzxU1>shSkRTj3rZz0 zcFPs6%p)1*g6s#LRUKo6D`iO_>ln^-*mF#`U9_gx+w0eTtG3rZ=y;)%jrNaIPm?#* zjfstS@pDKD#KpE^!n~ges4-X*oAHHGb4OkVPnBjLPp$-BisnWqTtQmn(b-SAP5s@o zLSsC=6UIkqD#k7Axiux#<^W}>xe<r-6ifcm-0m=`;@iU8>JTl>oGQiX7batZ`n<+*BoR!9Fbo_1kgIk z&5ixDCHVM?rph{=_v1I^M!e)oKC$&gK8an5X-~^e+q3xL`hkzq%QMa2vTb`Fb_euD zI4GRJKmC}-hUv|?8LtQ%JVkN;qOU&N+r4xstqVD^j7(2cz7Zs~DqZt;$=r4gKmq>N z&7q-Y2XN+fOX}0yv`_QGrYG|Hf4Ji zR{AN`PC9BCB4acr-S*(45{@_~zpp-czMLW0Ncdxgr1^ z2SoonHBGVVoa6Q_mwZ1YDylax`}(CO#OQMk%gK2T3Qo4RH@=h;t?tPqRp;%?`a<^ir2cmI6FMk z-mY`-@sI~$u+#CJ=|JQmu0x0z;}@Q~6wsrH>J#=zrqS$`*dh1G@jMMGc&F45SHfDOsC5B*lLy9qLN$dSAUGw2-ll|{n z7emzs2RK$#savvR0=UcWhf$a2#mO9UT6)*I!k@Betb9m;|9Z=DkA_GjcueHm12r@z z!aooZ?vEyeW=ED+pMgj51uWP{wtk3QDvPU%nkqInxpx1BQVZpsF4|`cLsGn64C!zk zBhJRcE0V6~GRjjIE>ng4Q)Gg12J&t$CRZ^`DE<_Q5bElrxU88tE zLz$GTi`HFoyZbd2by4w3GLS%*WYy<1lY8Q}H=HyQ0t}jXiZ{u z<*@B#kIXbhj<@So;EB{kfY+XN^ao)`0?FG7Gw%s_EVwrAiHHST87{TkWFl9H2^)%W z^7W)RJ8>lduS4M7^lF!>xD>Qm^Jq$8es8}@&bwxxWB2#)%T`{n4HDFeMwG8_N{l?x zthcE8TBx12xv^St=jB6!Y$}4ywQ*(CyH4ZeBSVSCqvZB`GQFe_Uk?=nT;FV;@4QkB0?J3Ek$7l#H>t?vYRZ zP7VkRP_&iQ{7`$hmsQS756_qdfO6NoQ(&7CiQ_HzF~D%j8k7 z+O6i+@yV)qN^SRJi>wY;?k`?+5?02+m`vkER=gsmyyHwDn;5Q;qT2bEdv*^?%B@RY8sfKlG%SjkgI5Bvzsckhz1^OL&#R&0dU_-I1l z)ic3ZLwWUQr;R5xg5<{}vM7l_!>gK454(qu!*k!d(fdk0+KDaq8kD~XPI0~7N3dEH zSZpdE1Wj|S{=&VKgoy(=95=WjaD#%19GiF78Rq6>x{qthjW5!got9>jP&=?DfjMO+ z2yJ@qc6AKs+J`W;yy|uic2~KA!PTNZZr;A{f^fLy_pm>^}ZR zL%CuVp`G*IX`dUq{^M~=@wyLh#^+x@QsFj^65M6Ra-serrTg{ScIK7(0?iNW61^u5 zBx#vMCR~E}=O#AS#2Ll39zMB#C^r82A|ILBL;v6hL z#vC6a`uOGpk9jr3FvFsj)28^QrT~%%R1tw((Zndq>;mWa7AGx|s$#KBkSVd!i$0cf zNrkTst~A^bzfmHGNN0|P|IjV7R82@zw$=U0g{peC#&wxz;E) zg7PmulxQ@Biug-wpK%ps8;VdMtn(G3g6*S-t$D+z<)HgsK86-&E41(qLwb+S&(;?w z(P;|ZPZ8VDp7tG&VX5)(~>Z?Yt21p%cRZHEu7Hzs11+^u3on~Y11a^?_u8y? zy-Zo_<(1oI$~_vnLDy&Y;+xoR$Fe{0;y|(|j3uG>Eu29+Zl0|SI zXS^b#2M`y`IG5d!v_Z{J`-oMl=Ny_zw>kZt)0=`z=G=B7Zg9Q@74>rnn&p$Nxsv&3 zTD;R)WrPI;wSU~cePp3MM?vvKPc_&&frlbb_ zlH#w*6i=#F-hS_L5v};DUHp8!fJKuhqgU>(>GdC+Qk(fpk4Ht)Yfp|kp=uiRLPG;6 zt)SiD5x2m@#D=c0(u~Fp-TXJBrLwD$YA40+mXzqR0H0G>!y`?O(*u(CySi)N;ijMD zl@3Wm=Xv1s91Afi*3POgrHXO~m=|#weyGcD?A<2lMZ)rjdAftX>+nx@wMLvDB(~*` zG8*tqgL6*!L9O6(H_5*_Sx|# z$}t+_8TV%nn&+6-@M_<+Hhd9%V%-8VF%eXEC zaA-0sd^36Mf4JLRg@51IXu>lwh0>cma}<5H7f-uM8Wm+0q#rD`%8L@QVUDplWtuE& zT*Y^`!n0!SMCH(vv0S^?s^KaE2kJ{r**>Q-_xLZfF0^b|n|hgOK8}Ci9@(mY^y~5W zR{cQ|DGvK@i=>Hlc+XRCd7_MO19`p5kMmuPuZXHV3~%FpEHYMFxXB_m8}RBeUBTlq zik@@XrQ%WH?3dI#V4_a?b*pcBN}KgVWWw#UdMjT(XLn@!e2#E+*ZHH7#kFnK4Jkr8 zEyode2o?${(wb3ub@@ea25kL4ysVlMXgP{(MxY8;%G9FW5}8&K6L}s1foE`p^5mu2 zyoplqOCnR<%{v^EI~cWI^~}OJ#DH4akr}ZM8B+1;*!531BH+3l@s$V+jxSuLcZp*D z!tzqJy?Ukj$9&ZEXO4x>{?CE$dA2HrLea5tJd}&A*5_MKguD@9ef2W)!fX}WoeFlX z>O&e9SSk;GWuSZ6@}Lpx2c{HMeT1)qLskNlL#aP4HOAv zMa_hRww9;6m`h7~z7Luknhowh-jJ)WpBaCO@5&lnLZmi(>E*KXV~N6P@Q7MoT;>DG zw5Yh2Y;8~5GuLiORW(wu(pfQ9r(Y~^`SvCuU$`Zl@_Q;*Kqey(oa$0jaBh&$%f{>< zGEwWT#rz-c0t+MJcF*5*>~@Yhooj3P;wcf}nz8O2K3V0ITM3z@<<_qn=@Go)x68zT zTCaSDt3YiMJapQ(zhZqj@W23H;%*~eNRJ}dq)DvRi{X0yTh0gDyH32&EV>U(YD_{Q zx+BZ7k>?ZAWrRf{WM_Hfou;yv8qW{MS$=qYPn)xN(K*jDwA70Gu8vPP5SzZk6~!=i2~ql$qb&UDrM5}+TmJdD zea*17(ri!4#Ik4sOWC#qg|8wl^Dj8?l`JY9t}j*}H6PcKOIIw>pb$EMTwWLaaV&gT zDXQX$(9@HB#Zm7!oMf#p4@sCXJ{0!)W@A?KT>8+53Pf`{-TQm%HpaOwF2?<@!wPH0 z70rh125C$;^x`M7>Zvdzf_src{QMsdx$NmG2{q4!>6;JbCXc+|J)Jj)s_51~(Jhf6 z1+}Hw48NLZB)k@VV?!eg8V&6#(Td6Vx>|BpE31s_q(rdoMRnn!N3KSJOp|#n?3Qac zQo_3$%FYM$x%hOP^Y#NAYDB-#HWnHkv(Ufd%xP1};DSXy^GnvHm6cKxwRyf_lM8i|6i>_s4HJ za~=>3=(B#{CxL%knR1PHjJCPZo`=!v2~A%E9_s$CQm)jN14MU zR;;aVjseCe@M)%eGUcB#m4==j*2#Vc+0 z*f}nL(;C7LJMd>s&8Ol4b|TQzf0(gmwDDz%e4fbo9i3P2?FrMPD{!rS5sbUd7Ilm! z4zqA0?Rt(;`CC4{eZtpv-L~3go-)_ z^xFL#j~mOAtWQVeKMwDEi#EaJwt9De9~Jl>9CFNuPX@zP7*MRiJ6gBO`C~1#h{HKV zu58U_S$4Q;*mK@)B!s(YL+1^=`orle#-7-;=0XSW#QIww8)$u&rt{>!B-~H4>CLQrw=X{f2h)0 zylyGFJ9khKY1}Fqd?suHgXZE2Or@KEh8>Fm1r_=gSd#?6qQmfU2;)B-K_f@KV~> z6SG~F(+B_$U6iuCct@Mj^-J3BB{n;nSQ>3TORN5iwCKuPxFngCixCP#-ImEjV5&93 zK!DgZ(z#*lWq9GMLQds3v}cw5#=!UX6hw;`f41U1{lYBXRHGX#C&D6_wRpUw?;okP zEy{A!>MCG+!*9R&@|6w2`KaA9ovUXlBbQ=Ao&9|AKL%YyEnnlMcvhzx^1~#J77wS5 z@t2P8)3$*^5An;TO3e(<6#9O9R8v?$`jCuW|K)MF%qcqs4R+(YmGDL?NP>*{)78Zw_#J+1OvDJTR zfVR?j9L~9rq$SW2VwPAH5G5r1B}t)TQee7Z@U`XqKBw#m>{EldM&DY6x3aP&uSC+> zr)kfzMi&zLVTm6$vF*y&GxvUOg56Ysz|3P)mf+zb?JVjjM|E&AO3$&rm~4Y|3A~Fa zqmGM`=x%oo(`FnDy>fqWG1$|6^h-lz*FoAB@fckbmxFF7tE^*Xq%$A_@TY6TT+Zkte-k6nw&h zkfAd}Z!kHP-H#&G9HQajR^P9y@+?Qv_bWaVYPiJ?87Or6K?EdcsNm%)r@CuTqNz6A z4)Qi7NUw4oK?E&1sT`}!Hic4OrZIM+X@5LyQ;2S8oZf$ANQY7Qc-2bEk+XJk?;Urs zWL;K1hHk$xa3r?nf=HTu;{Eeg`;2d8S1n{Ah`=57`yCtZ``UD)M{CvCJ|%P>T@An4 z;yJxIqtv@g+BT}g|81X7=O4F^R6>&L&;S(efVzqMr>HXrcS5* zbz<`{N4<#1jc-sKs+usEp)*x1j%Yn_rC{IN0hKCoVYFASg^RaOg1?GBp1IaNaiesz z?$gNe>cg+SkJ3<0pmKSGGCD@hAGqrCoD{(cv;a7 z7-d$2GSPQbFV1~9KkLpaN@+Z3SAXPvAlqR=q|cL0lcmw3!H;&Vq9W1jVm1Z`HVDOi zj!u#9n)ivWy6e{02C-gs8NL>UsNf59wkHG@;bN5~QG0`qrkt=3#FZVsG^{KC;Fchg zId1AzZ{4*hO||qQBA~#ot!aV_4nnm^)n!|En(#}^yh`&L&SB~^_Tu5HHg$HZzesyM z93Q0J2fdzmVb*tiL(5y$2@VT?!Trr>(}8mM{_0crs~>G|sH)v`VH;hf6|%Rv@jz}f zDd3AX>V(Ep@SE|))%g!*z8(XIE7x@cxR;Lcj`rX^c$z+)bqK;lBYcJqJZNI`(f5Yn znNE7#8=NYe@VmHIP5APz%{IP3|8IE@{lEFYi5r<{e0xWwg{>RYZD(H+8t8Meu_f8i z!=aS7zx>gUf_3f4g+^S{=dy3lxgHN8`WJ}69KSf|tEfyW&)MilA%xXG-7+}(?Alo+ zNxpv1g~qcFlqb7GSt$ueXA7<=CCJ#Cl;kwN;W-d`7Pe;{VK5p$fy`iZz)SiDfiTi@SxA;p`&7v$3$UAY$GKGW3 z7Figk!UpZrcD({oDL5UR2Y#GqG=H9#lkUc#slyWF^e$eCjCrgrqAW72(p1-T&{Y-3e$JvNQj94V zs7`mCRbz8?8MJ$Lc*rc|o1S36!-s99gD)lCjbHw5{FW!NjB46jxI5e3V({KVr)ITt zOR9cs8b!{zOna2o)69%Fi$!_Om5z*~-ut?Z?!=BHexl+h0@3TGUusnh-`)&8IFahC zyxuPPkgehst`ssS?|L&QbjEf)-NGd^LK>SZsUbt-vN2R=<+w0 zw{t+h+|m)2S9YNUu}}YB0D~HQ<^KSXc|6l9-PGBT5%Ama_fUUt7zbA9@C;UxOf->r z4+v!+y?U>*aZR2ad}4fvQVACOffjB&yK644w2&1t6XL$>aIq%c-L(~P&fNp8w9uRW z&(}!Xc@gB}uyR9<2(|snd)v0A#Y&!w z&4G24FayGXToLUx7nRDfdU(f?AORE*ct7mDWz$IdC)27^q+=T`#c5|JCIfeN4qEM*jfu++N&3zb|JsVO~UfwVdTqa)!~c z@b0k#@b|C#YJVn-4``0jpf@YazSMqAtYv{F;g^k%eFpX95?C$Oz7W*3hKtQvbUp$# zE!o3f+ji2bhK-ejUv;ht;5)@k4H=QuiVGFum((A;r-hoHVGucUCCdU*KZ$vY)9b0$ zjU)`}!GSBPk)`(o;WSBbP$pCoUA5e|9^PWFnwpXH$5M+PbrKLvnlBJ<-^RAaSLJ)g z>k7q2;sMgbU#9go1&K?O+hNH^GNiM~-A~ZL5Zu@*9 zh^)oTWMaRQBOIIV?9+@(>=+pU9>#BqEUzooiyHiFkFB-`cM8GmQAcMez zN)naVeLEB`Wk|t*=)twO(|UNR(zh-YDK^DpZ~J7OH2H7Kx}wICvbaY2a84<~fgDGD zN$KvN(&lr$2Z3+68~i=gu=H6l<5ReT@tpxW5%kl*!(vLZDu#EA*cDa)4q)j)EPWf( zSu!$|1%qGW0FV!Ew5+$`QsbohW76>B6_#knm~C-kTl&2pn#bGk{6|sgYiv~j3kv~W zocMXyG2LraV1NJC;z(3If$$r&e9AXHIjeS8k*`$rGP1zzJe4MI)^~A;ik)iX80>GQDocrqjiZn8D^YEsSFgdfbs8W1Hy8u;Jon@X? zUOfvt24Bn18%E<*aj0NA{eqGV0fM))lw3-m+AaBbGZTR7UAqYl)kZFYQ{H1#8=(LxH6u6wWWB6_o!)QHe`DAln#C--=nku)A4MPLKhbgN*pVPr zRR9eE0=MUuU39TxEr7A%TS&{QwP0=X7eWq|yCWs38&Y`C8V6CwjRy0ZgK7a;Z@b@B z2o^ovwM5z?K=%)_f)qOdZP`&_V_%!`Uf1{15C7NU)5<)>kP@yePTg($YsimO+${3# zXl>?cxdxi{i}vDSvfR7sVzE%k!Dt z54V+VI@lt^(fU?TNkq=Zg{R6Q`^ep}9c{Gm_f+-Op=9a(Yt$Keuo<9Z7T@MJ(xY!^ z@2vcci+mYa!^#2~n6JZeVm-BZl%^b!83_vMyZ-=C_)oHo17>7J759*=1;Jh)MHmrJ zH<(MKh97#@A5|doC>rh;9KoPAN$0)SYXEydKSeAv&}6XyvXS06QzXYun^CTvl~Ah7 z)7a>5$FjEOGqpX3cXd(3o)xz-ovT(eZHy=;!rB_u1|v5B_Y+8K!~$(V)`uGA^wX>u z02C@x?zV2-Xgvki{{TUzP0}-C-D*~mw8jj+;6A#}(qr<2b^G|#=|BI_;LFapsP{D!lK7jE6_8V(&_R_qK8f>Xtfhy`W40RsnAFhlzT5YkA`jB|@ z1K&jQ8kqx)tEl8TTeymN3`ECp+zpB4uD$gZ959YH9poLScI`MCn-#5ESj!dMx3SOz zb>=ELG3chtTsg)#EXsp{B?I!>QwEYEX_0N*lxM>tePs(E27!+!ot!2N(B%Pn`{B-5~iyp*_7BbBQjTST) z{NGTv*&F`=mjFM~-Tr@>zU5xCu_@EKt44^=vJ6KT!@*Igj`s5(_nY9qj#lw}H63yy zS0S+{w&+v^;^g>=9rd0nP%@;zOnDJNQzfpV=HzhWPnde&rS+DN!#FXqxDR`;4GmR< zu;TTUDx^(&Etx?Y_xnXo5>Fec0zqk-%kgRew|zVY8`KcQTgd+a$ZZdnNw>JtwnLJ< zol0!2Rmw2`0Jh*dx*NAZ8} zP!D1>#DWNaWz_u!i3Y6wF)S6YW7unMc}G=b#BpKI?X6^GY03KPbl{{Xn=!W7`409! zDDSN&gnW;K>ujgW5VOYY!+C-|NMGLJPkfz=e^2XpSvF8iUJH*i!>?sqI+HJss2{hN8 zswoTR5Ada-|IzvUvTnYiTTV8y?F84Gk4d|;r5!ot%-q{v5ZB_ zvf@Rn*j7sn77mfbv{8BfGs?!CD{N1s^ zxgiAEO^GJfF63y;2L2jsQ7BUROTi+R%^@zQZ?CB8-c{Ozh!6CG@3?){{S^^ID=4Z zWMSNmny+VlD^(tg1fLRd0F8U;eUB1&k-TIFQbO?uUO%)_`%#d_{{ScYV6u^QCygId z)?@23#^JWvufvh|8hDW(sm(iX3ZY4~k+8V#J*)gQS}`MuJi%b=ec~xO*QwA9Uzb<^ORXI(oP6HbNPXSwV8PvQvQNHGUSBtO z6|dXFKd+VN(|_E29JOe(UGBCbc0z=1*372gRX%!^9dTmeht#@$YVO6zNxPJbk9AW# z1E{iqM?2NXNRZ=yzLhr3k^nOI(h?LHlZek;y8l1P4aA0d&`p1Bs<(n-XqV!l~X^ z^IL1#xOa1>3Xdi>SLI{!)60ppj>p8zPNvL$6HD?50JnEcM)G5-K9R;La0xWGWdQU&&*AaLTxPUWfV=Xju#og@x0IH4Bz@8=BCdMYiR<($`3*L!9xaY~7>R7W}0(1&AH>YZGYQ!=uiu)(*Du0C(uRT7iLl~t}s5o7uqsY;}x$SMZi zEyAl)fc1W%l)XL1IBmyCHt4}mI~uRCVxrkuLMM(t2F+aDDmLyb_+0b(YA?e`f2MUV znz$_|P-7HPw%;xjZSD7({{RiCb7Y=H)bCE~xY1*XanQ#SsCDy$769r@hnd%vRQ6}V zeG)PC#Yt7-k{ynte4B!zpyy*|X1@ie_b;kfr1dTGlb1SAk0IRzikDk(YlGq%d{n;w z0G&I+V0CFaK2&+wkvxZR5_xO_fPNFh;iuPmzpZ)g(dcr_SIAtmgRK)HKn1n2{8LAC>!$gUw^vZ`jG$A`Lbtn>>E?a z*Do>OM}|nGj5{D#Tad@^4m$VLS!Eh;24dF%LXAOQRy;uLsO~CPoo_C}ZP;4tuWiQ~ zM$2vT!;%4AJky1ga-tljW^u7#5Rb+7+ z_@gBmf!%9rZ^+()5+pJsW$PG;d@{%wc>WdIx4rIe7OH%eCmi6J0njCtNEa)Ah_UA* z_k2GMS5x4*zDYP18%A-k?Gci5} zB->H0yv;kAQRxQ z7h~P~#+3_1Yh)Y<<9bw#EI`{$O{`5L88Nq99W~eWngE9w9c`yR`%nx=6pLb4!+)yO z>?me|+APA$03^;d+G_kmx0i}mwqed`t8(lrBX=uhIWu69# zP#s9n9w7Hxu2QO(P3gJaM=pFwWd2kei_It^J@@db=dX1>hbt#b=&|BJqibZ#7$!#8 z0A?HxylwLS8qZXh$IqEs(vrkou7hx}9}5w{e6)Sf>S8468E_bm0)AbPi4ryOqj3ct zb<>qUw566D=%18^2giyzPmo9hAO=%@BI3t|IiCe}Efz0Vks0tMNn-?gaF*EWr;3tv zHt+bBoR)kR&&uiO{4$8`LfCe;B-D6d$@7uT(0IK0JCmy z2^Q?s@dk<_Ljgd(^ko2PBHvNpZ7V*G4>gr9q>=8qaMmDq4pk~Iku*#^9vl3p?cu_l z2fWnu=Z1+Ol{s&jk|ndtcpMv?H#|nORtd);gkW>U|SoYI{5hoUKhF4 zwJHwHy|C0r6EKDLqd8!vKnU=u)z0EgGSp;|#}sBZTkR!_HohUp!Zg$F)6*d=GAgvi zm=ojJ2gTlRSIwUZu_@6Xb1Zgd5-TP4js#lf#@Z3vQ{3_;@X;79=$%Lfwz0T2*Ow1< zMO+lH4hbG-k_eG52ZW1h-`h$RY7$Qn19JiZ=63M#+m$*-;q&9L3_Q>{k}uu`xKg2Q z=yUnNMo)}d;E-?r6ctUjC}0X|dwJA{yqO{bz*v3nwv7#g_-sz3*iZ%;0d5Zw#CB_H zpdw2dUkTJ(Q{POQ%OYvor$a`Ey!Rk@uig78>0pt)+h`=Q;oEvN8j}Ygs_$i8#@pD@ zqXcXTG2$&NEQ{EuUu{3O2^ohOVlaerh+Lm3ZMjYLV%FzVdi<(Hk)I|5vQ3gSekHNm zb|1Tc4z&K%v9u*~3*^KK%@DX33%mjG6&h~;0CiT3HPIWV$nr{5#9Rg?2>^RR(^{Xj z(PcjdNb_XH$i`;?0T4~0*-oXw2ZgxPozZzY&?Fd;x=Xe^eXNpeZB6Zd{{VeY7Vq?Z zNSIuwSumauGEz1PGB=CDHCHF*UzNWLR+jiiI_@||f;S1Xa)YE=-%JO_dLIiEyf4y1t z{h6%hnPLTjTk})8-%k-y6%o%Xxh6&`tI0*1+1*uvm|{>vviL#V9FT3;JIxk@pZOBV z0w`8s0w0);XK2&->Su^)d`t#5^Fa=vd2xkPe`p>Rr3I~etH!wKv9;uFHx{+P7Pz+r z9i!r=NSA`e8Ycdbu-jml?Z0KuI(w#4NOG2Na(0;5knB#k=WA=-S2OYQky|EAUZE6` z?N!svLb?lEjr+gL^3$GTZ;F0*mE&2}N^T@=OEBGSbvN*zinFuJO)S$Fjhgo*NNt31 z9lCR(LbP~gi9)i4-~oGILB_)W0P3QHaXd_XE3m>mjwDk}iEk}f0mL5L8dx5i1|S2W4#bOEo&r+z{I7~MFMc*0X#ZXpSWsKypwhCQQ?c&e0)!352Q`Ef~jB0Hv8IIk}aO!95A z@!`rw81fgwVxdnT=Gx=~*tJ?%XQRi3g@PoWa=v1;Y+s0R@Ysr(PYo>CDy=3wm&bjg zWdiI)u1fcEq6Q_;P|-J<0G2x;;Oa>`KqJL{qL@ubg%E-hk>X~&?)JKs8f+thE&iHv z(oOVCVU7IK#WAP;oFw=`Cq)hq*r8dQ7MP04^SWf*7>2W z;XE#E#GBY(vbp1~yrN53i#^D{vWte4z}!`VyI$Ab;{9Jxth)J8`IQh_M;q;+UkZ!% zsJ^u|Q6DOj#!HZZXaE2ZIqO8AXtAYu!&_i&Y@^MgqRj zq>u-O=iy(&M5-a3fM$X~ArY)A;cM?;M`^GXeB7zNNn;#|FcoBja;Q6<&ZUyzp59`# z>&Hmv;KxbndHHxtjT}zoJ(TJ+;;qWtXzKYB;bO(~FH*>Xzcj)ug*OCtj(oMO_9z4B zKD!s2BC|mfF!IZ@|w zLc&sPqWlS?L5`7;14v$N7-U&G zSS~W@;TU9P-wtT{6&bn!O7gCFFnG+)Y>t4h4(%295%nYf|l8L?byD&E$ z^`BoViMI6bP$5W&vnaOi3Qz8eil|CoPxW8S10)-n&G%R}zQv~?)T;V#F*XgmgKBpY zN&P#i9H%7o#G)WfDo-WkKCMkG8kFgsA0ilR`ZH;#hfe+^(WOs6NmCVq`0Zk87%R}R zP%oY?oYa6p_fj!G;<{9?<&1DtSlsLO(e^AAx<^Hfl3_p^+xTm0KGb2eJ{B};abZwC z@Dy4wtcF~PEgRj~-GxcLkJOhNC8EgFX8}*hKMz%e!6d}o{Gd^Qt4TIgci+c{XrhAg zV+EUJ(w8HE<7$x1GWABx3lUPQ+s3FG%9}Dd0g@ok_)P)fHV0FL!~r&4rkHwH6} z>28?CnOn?=8-~BtMvOe&KMxK)`#_KG+fz&Az!`FwUN0%0CN>3mQ-x6Q^og^&v#8wO zSp>%10jFriucu<+ra)}hQ!FY#w*hWKmYA{(7s_eoJ04;dC|ik3akVlZ{xcF@my%sC z6U4z#ec`AgoS_5Jv7Ri1k(kAiDDul&!h6jio55+40UXYfq)9?8Vd;b9Gui0Ik{5bE)ie&Wx>I47T8ucyd literal 0 HcmV?d00001 From d137f8bb60af9bee04fa606783b8a47da085fc8f Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 26 Mar 2025 18:06:08 +0100 Subject: [PATCH 030/701] [DURACOM-243] Adds rotation handling inside JPEGFilter Conflicts: (cherry picked from commit 08e330c1c0a62ecfb83c2e7617472bd5a50f8c5a) --- .../mediafilter/BrandedPreviewJPEGFilter.java | 23 +- .../dspace/app/mediafilter/JPEGFilter.java | 274 ++++++++++++++---- .../app/mediafilter/PDFBoxThumbnail.java | 1 + 3 files changed, 231 insertions(+), 67 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java index 7b082c6c21a4..483e4f5f6ea2 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java @@ -7,9 +7,7 @@ */ package org.dspace.app.mediafilter; -import java.awt.image.BufferedImage; import java.io.InputStream; -import javax.imageio.ImageIO; import org.dspace.content.Item; import org.dspace.services.ConfigurationService; @@ -63,27 +61,20 @@ public String getDescription() { @Override public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) throws Exception { - // read in bitstream's image - BufferedImage buf = ImageIO.read(source); - // get config params ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - float xmax = (float) configurationService - .getIntProperty("webui.preview.maxwidth"); - float ymax = (float) configurationService - .getIntProperty("webui.preview.maxheight"); - boolean blurring = (boolean) configurationService - .getBooleanProperty("webui.preview.blurring"); - boolean hqscaling = (boolean) configurationService - .getBooleanProperty("webui.preview.hqscaling"); + int xmax = configurationService.getIntProperty("webui.preview.maxwidth"); + int ymax = configurationService.getIntProperty("webui.preview.maxheight"); + boolean blurring = configurationService.getBooleanProperty("webui.preview.blurring"); + boolean hqscaling = configurationService.getBooleanProperty("webui.preview.hqscaling"); int brandHeight = configurationService.getIntProperty("webui.preview.brand.height"); String brandFont = configurationService.getProperty("webui.preview.brand.font"); int brandFontPoint = configurationService.getIntProperty("webui.preview.brand.fontpoint"); JPEGFilter jpegFilter = new JPEGFilter(); - return jpegFilter - .getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, - brandFont); + return jpegFilter.getThumb( + currentItem, source, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, brandFont + ); } } diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java index 502f71eb5ca8..181e3bcc4b58 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java @@ -8,22 +8,36 @@ package org.dspace.app.mediafilter; import java.awt.Color; +import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Transparency; +import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; import javax.imageio.ImageIO; +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataException; +import com.drew.metadata.exif.ExifIFD0Directory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.w3c.dom.Node; /** * Filter image bitstreams, scaling the image to be within the bounds of @@ -33,6 +47,8 @@ * @author Jason Sherman jsherman@usao.edu */ public class JPEGFilter extends MediaFilter implements SelfRegisterInputFormats { + private static final Logger log = LogManager.getLogger(JPEGFilter.class); + @Override public String getFilteredName(String oldFilename) { return oldFilename + ".jpg"; @@ -62,6 +78,134 @@ public String getDescription() { return "Generated Thumbnail"; } + /** + * Gets the rotation angle from image's metadata using ImageReader. + * This method consumes the InputStream, so you need to be careful to don't reuse the same InputStream after + * computing the rotation angle. + * + * @param buf InputStream of the image file + * @return Rotation angle in degrees (0, 90, 180, or 270) + */ + public static int getImageRotationUsingImageReader(InputStream buf) { + try { + Metadata metadata = ImageMetadataReader.readMetadata(buf); + ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { + return convertRotationToDegrees(directory.getInt(ExifIFD0Directory.TAG_ORIENTATION)); + } + } catch (MetadataException | ImageProcessingException | IOException e) { + log.error("Error reading image metadata", e); + } + return 0; + } + + public static int convertRotationToDegrees(int valueNode) { + // Common orientation values: + // 1 = Normal (0°) + // 6 = Rotated 90° CW + // 3 = Rotated 180° + // 8 = Rotated 270° CW + switch (valueNode) { + case 6: + return 90; + case 3: + return 180; + case 8: + return 270; + default: + return 0; + } + } + + /** + * Helper method to find a node with given name in metadata tree + */ + private static Node findNode(Node node, String name) { + if (node.getNodeName().equalsIgnoreCase(name)) { + return node; + } + + Node child = node.getFirstChild(); + while (child != null) { + Node found = findNode(child, name); + if (found != null) { + return found; + } + child = child.getNextSibling(); + } + return null; + } + + /** + * Rotates an image by the specified angle + * + * @param image The original image + * @param angle The rotation angle in degrees + * @return Rotated image + */ + public static BufferedImage rotateImage(BufferedImage image, int angle) { + if (angle == 0) { + return image; + } + + double radians = Math.toRadians(angle); + double sin = Math.abs(Math.sin(radians)); + double cos = Math.abs(Math.cos(radians)); + + int newWidth = (int) Math.round(image.getWidth() * cos + image.getHeight() * sin); + int newHeight = (int) Math.round(image.getWidth() * sin + image.getHeight() * cos); + + BufferedImage rotated = new BufferedImage(newWidth, newHeight, image.getType()); + Graphics2D g2d = rotated.createGraphics(); + AffineTransform at = new AffineTransform(); + + at.translate(newWidth / 2, newHeight / 2); + at.rotate(radians); + at.translate(-image.getWidth() / 2, -image.getHeight() / 2); + + g2d.setTransform(at); + g2d.drawImage(image, 0, 0, null); + g2d.dispose(); + + return rotated; + } + + /** + * Calculates scaled dimension while maintaining aspect ratio + * + * @param imgSize Original image dimensions + * @param boundary Maximum allowed dimensions + * @return New dimensions that fit within boundary while preserving aspect ratio + */ + private Dimension getScaledDimension(Dimension imgSize, Dimension boundary) { + + int originalWidth = imgSize.width; + int originalHeight = imgSize.height; + int boundWidth = boundary.width; + int boundHeight = boundary.height; + int newWidth = originalWidth; + int newHeight = originalHeight; + + + // First check if we need to scale width + if (originalWidth > boundWidth) { + // Scale width to fit + newWidth = boundWidth; + // Scale height to maintain aspect ratio + newHeight = (newWidth * originalHeight) / originalWidth; + } + + // Then check if we need to scale even with the new height + if (newHeight > boundHeight) { + // Scale height to fit instead + newHeight = boundHeight; + newWidth = (newHeight * originalWidth) / originalHeight; + } + + return new Dimension(newWidth, newHeight); + } + + /** * @param currentItem item * @param source source input stream @@ -72,10 +216,59 @@ public String getDescription() { @Override public InputStream getDestinationStream(Item currentItem, InputStream source, boolean verbose) throws Exception { + return getThumb(currentItem, source, verbose); + } + + public InputStream getThumb(Item currentItem, InputStream source, boolean verbose) + throws Exception { + // get config params + final ConfigurationService configurationService + = DSpaceServicesFactory.getInstance().getConfigurationService(); + int xmax = configurationService + .getIntProperty("thumbnail.maxwidth"); + int ymax = configurationService + .getIntProperty("thumbnail.maxheight"); + boolean blurring = (boolean) configurationService + .getBooleanProperty("thumbnail.blurring"); + boolean hqscaling = (boolean) configurationService + .getBooleanProperty("thumbnail.hqscaling"); + + return getThumb(currentItem, source, verbose, xmax, ymax, blurring, hqscaling, 0, 0, null); + } + + protected InputStream getThumb( + Item currentItem, + InputStream source, + boolean verbose, + int xmax, + int ymax, + boolean blurring, + boolean hqscaling, + int brandHeight, + int brandFontPoint, + String brandFont + ) throws Exception { + + File tempFile = File.createTempFile("temp", ".tmp"); + tempFile.deleteOnExit(); + + // Write to temp file + try (FileOutputStream fos = new FileOutputStream(tempFile)) { + byte[] buffer = new byte[4096]; + int len; + while ((len = source.read(buffer)) != -1) { + fos.write(buffer, 0, len); + } + } + + int rotation = getImageRotationUsingImageReader(new FileInputStream(tempFile)); // read in bitstream's image - BufferedImage buf = ImageIO.read(source); + BufferedImage buf = ImageIO.read(new FileInputStream(tempFile)); - return getThumb(currentItem, buf, verbose); + return getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, rotation, + brandFont + ); } public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose) @@ -83,25 +276,28 @@ public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose // get config params final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - float xmax = (float) configurationService + int xmax = configurationService .getIntProperty("thumbnail.maxwidth"); - float ymax = (float) configurationService + int ymax = configurationService .getIntProperty("thumbnail.maxheight"); boolean blurring = (boolean) configurationService .getBooleanProperty("thumbnail.blurring"); boolean hqscaling = (boolean) configurationService .getBooleanProperty("thumbnail.hqscaling"); - return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, 0, 0, null); + return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, 0, 0, 0, null); } - public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verbose, float xmax, float ymax, + public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verbose, int xmax, int ymax, boolean blurring, boolean hqscaling, int brandHeight, int brandFontPoint, - String brandFont) + int rotation, String brandFont) throws Exception { - // now get the image dimensions - float xsize = (float) buf.getWidth(null); - float ysize = (float) buf.getHeight(null); + + // Rotate the image if needed + BufferedImage correctedImage = rotateImage(buf, rotation); + + int xsize = correctedImage.getWidth(); + int ysize = correctedImage.getHeight(); // if verbose flag is set, print out dimensions // to STDOUT @@ -109,77 +305,53 @@ public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verb System.out.println("original size: " + xsize + "," + ysize); } - // scale by x first if needed - if (xsize > xmax) { - // calculate scaling factor so that xsize * scale = new size (max) - float scale_factor = xmax / xsize; + // Calculate new dimensions while maintaining aspect ratio + Dimension newDimension = getScaledDimension( + new Dimension(xsize, ysize), + new Dimension(xmax, ymax) + ); - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println("x scale factor: " + scale_factor); - } - - // now reduce x size - // and y size - xsize = xsize * scale_factor; - ysize = ysize * scale_factor; - - // if verbose flag is set, print out extracted text - // to STDOUT - if (verbose) { - System.out.println("size after fitting to maximum width: " + xsize + "," + ysize); - } - } - - // scale by y if needed - if (ysize > ymax) { - float scale_factor = ymax / ysize; - - // now reduce x size - // and y size - xsize = xsize * scale_factor; - ysize = ysize * scale_factor; - } // if verbose flag is set, print details to STDOUT if (verbose) { - System.out.println("size after fitting to maximum height: " + xsize + ", " - + ysize); + System.out.println("size after fitting to maximum height: " + newDimension.width + ", " + + newDimension.height); } + xsize = newDimension.width; + ysize = newDimension.height; + // create an image buffer for the thumbnail with the new xsize, ysize - BufferedImage thumbnail = new BufferedImage((int) xsize, (int) ysize, - BufferedImage.TYPE_INT_RGB); + BufferedImage thumbnail = new BufferedImage(xsize, ysize, BufferedImage.TYPE_INT_RGB); // Use blurring if selected in config. // a little blur before scaling does wonders for keeping moire in check. if (blurring) { // send the buffered image off to get blurred. - buf = getBlurredInstance((BufferedImage) buf); + correctedImage = getBlurredInstance(correctedImage); } // Use high quality scaling method if selected in config. // this has a definite performance penalty. if (hqscaling) { // send the buffered image off to get an HQ downscale. - buf = getScaledInstance((BufferedImage) buf, (int) xsize, (int) ysize, - (Object) RenderingHints.VALUE_INTERPOLATION_BICUBIC, (boolean) true); + correctedImage = getScaledInstance(correctedImage, xsize, ysize, + RenderingHints.VALUE_INTERPOLATION_BICUBIC, true); } // now render the image into the thumbnail buffer Graphics2D g2d = thumbnail.createGraphics(); - g2d.drawImage(buf, 0, 0, (int) xsize, (int) ysize, null); + g2d.drawImage(correctedImage, 0, 0, xsize, ysize, null); if (brandHeight != 0) { ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - Brand brand = new Brand((int) xsize, brandHeight, new Font(brandFont, Font.PLAIN, brandFontPoint), 5); + Brand brand = new Brand(xsize, brandHeight, new Font(brandFont, Font.PLAIN, brandFontPoint), 5); BufferedImage brandImage = brand.create(configurationService.getProperty("webui.preview.brand"), configurationService.getProperty("webui.preview.brand.abbrev"), currentItem == null ? "" : "hdl:" + currentItem.getHandle()); - g2d.drawImage(brandImage, (int) 0, (int) ysize, (int) xsize, (int) 20, null); + g2d.drawImage(brandImage, 0, ysize, xsize, 20, null); } // now create an input stream for the thumbnail buffer and return it diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java index 3acb6900dbda..577f1dec4a18 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java @@ -81,6 +81,7 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo // Generate thumbnail derivative and return as IO stream. JPEGFilter jpegFilter = new JPEGFilter(); + return jpegFilter.getThumb(currentItem, buf, verbose); } } From 2f477e370629935c09bed100f8dc791d8e052f2a Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 27 Mar 2025 12:38:59 +0100 Subject: [PATCH 031/701] [DURACOM-243] Adds Test for JPEGFilter (cherry picked from commit 82d04061c084486b92c9f643b96f59ab81fd4a87) --- .../dspace/app/mediafilter/JPEGFilter.java | 53 ++-- .../app/mediafilter/JPEGFilterTest.java | 270 ++++++++++++++++++ .../dspace/app/mediafilter/cat-rotated-90.jpg | Bin 0 -> 36813 bytes .../org/dspace/app/mediafilter/cat.jpg | Bin 0 -> 36813 bytes 4 files changed, 290 insertions(+), 33 deletions(-) create mode 100644 dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg create mode 100644 dspace-api/src/test/resources/org/dspace/app/mediafilter/cat.jpg diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java index 181e3bcc4b58..2ccc2afbb2d2 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/JPEGFilter.java @@ -37,7 +37,6 @@ import org.dspace.content.Item; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.w3c.dom.Node; /** * Filter image bitstreams, scaling the image to be within the bounds of @@ -117,25 +116,6 @@ public static int convertRotationToDegrees(int valueNode) { } } - /** - * Helper method to find a node with given name in metadata tree - */ - private static Node findNode(Node node, String name) { - if (node.getNodeName().equalsIgnoreCase(name)) { - return node; - } - - Node child = node.getFirstChild(); - while (child != null) { - Node found = findNode(child, name); - if (found != null) { - return found; - } - child = child.getNextSibling(); - } - return null; - } - /** * Rotates an image by the specified angle * @@ -261,14 +241,20 @@ protected InputStream getThumb( } } - int rotation = getImageRotationUsingImageReader(new FileInputStream(tempFile)); - // read in bitstream's image - BufferedImage buf = ImageIO.read(new FileInputStream(tempFile)); + int rotation = 0; + try (FileInputStream fis = new FileInputStream(tempFile)) { + rotation = getImageRotationUsingImageReader(fis); + } - return getThumbDim( - currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, rotation, - brandFont - ); + try (FileInputStream fis = new FileInputStream(tempFile)) { + // read in bitstream's image + BufferedImage buf = ImageIO.read(fis); + + return getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, rotation, + brandFont + ); + } } public InputStream getThumb(Item currentItem, BufferedImage buf, boolean verbose) @@ -354,13 +340,14 @@ public InputStream getThumbDim(Item currentItem, BufferedImage buf, boolean verb g2d.drawImage(brandImage, 0, ysize, xsize, 20, null); } - // now create an input stream for the thumbnail buffer and return it - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - ImageIO.write(thumbnail, "jpeg", baos); - // now get the array - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ByteArrayInputStream bais; + // now create an input stream for the thumbnail buffer and return it + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + ImageIO.write(thumbnail, "jpeg", baos); + // now get the array + bais = new ByteArrayInputStream(baos.toByteArray()); + } return bais; // hope this gets written out before its garbage collected! } diff --git a/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java b/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java new file mode 100644 index 000000000000..1181dc7a60f0 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/mediafilter/JPEGFilterTest.java @@ -0,0 +1,270 @@ +/** + * 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.app.mediafilter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; + +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import org.dspace.AbstractUnitTest; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Test; +import org.mockito.Mock; + +public class JPEGFilterTest extends AbstractUnitTest { + + @Mock + private ConfigurationService mockConfigurationService; + + @Mock + private DSpaceServicesFactory mockDSpaceServicesFactory; + + @Mock + private InputStream mockInputStream; + + @Mock + private Item mockItem; + + /** + * Tests that the convertRotationToDegrees method returns 0 for an input value + * that doesn't match any of the defined rotation cases. + */ + @Test + public void testConvertRotationToDegrees_UnknownValue_ReturnsZero() { + int result = JPEGFilter.convertRotationToDegrees(5); + assertEquals(0, result); + } + + /** + * Test getNormalizedInstance method with a null input. + * This tests the edge case of passing a null BufferedImage to the method. + * The method should throw a NullPointerException when given a null input. + */ + @Test(expected = NullPointerException.class) + public void testGetNormalizedInstanceWithNullInput() { + JPEGFilter filter = new JPEGFilter(); + filter.getNormalizedInstance(null); + } + + /** + * Test getThumbDim method with a null BufferedImage input. + * This tests the edge case where the input image is null, which should result in an exception. + */ + @Test(expected = NullPointerException.class) + public void testGetThumbDimWithNullBufferedImage() throws Exception { + JPEGFilter filter = new JPEGFilter(); + Item currentItem = null; + BufferedImage buf = null; + boolean verbose = false; + int xmax = 100; + int ymax = 100; + boolean blurring = false; + boolean hqscaling = false; + int brandHeight = 0; + int brandFontPoint = 0; + int rotation = 0; + String brandFont = null; + + filter.getThumbDim( + currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, + brandHeight, brandFontPoint, rotation, brandFont + ); + } + + /** + * Tests that the rotateImage method returns the original image when the rotation angle is 0. + * This is an edge case explicitly handled in the method implementation. + */ + @Test + public void testRotateImageWithZeroAngle() { + BufferedImage originalImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + BufferedImage rotatedImage = JPEGFilter.rotateImage(originalImage, 0); + assertSame( + "When rotation angle is 0, the original image should be returned", + originalImage, rotatedImage + ); + } + + /** + * Test case for convertRotationToDegrees method when input is 6. + * Expected to return 90 degrees for the rotation value of 6. + */ + @Test + public void test_convertRotationToDegrees_whenInputIs6_returns90() { + int input = 6; + int expected = 90; + int result = JPEGFilter.convertRotationToDegrees(input); + assertEquals(expected, result); + } + + /** + * Tests that getBlurredInstance method applies a blur effect to the input image. + * It verifies that the returned image is not null, has the same dimensions as the input, + * and is different from the original image (indicating that blurring has occurred). + */ + @Test + public void test_getBlurredInstance_appliesBlurEffect() { + JPEGFilter filter = new JPEGFilter(); + BufferedImage original = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); + + BufferedImage blurred = filter.getBlurredInstance(original); + + assertNotNull("Blurred image should not be null", blurred); + assertEquals("Width should be the same", original.getWidth(), blurred.getWidth()); + assertEquals("Height should be the same", original.getHeight(), blurred.getHeight()); + assertNotEquals("Blurred image should be different from original", original, blurred); + } + + /** + * Test case for getBundleName method of JPEGFilter class. + * This test verifies that the getBundleName method returns the expected string "THUMBNAIL". + */ + @Test + public void test_getBundleName_returnsExpectedString() { + JPEGFilter filter = new JPEGFilter(); + String result = filter.getBundleName(); + assertEquals("THUMBNAIL", result); + } + + /** + * Tests that the getDescription method returns the expected string "Generated Thumbnail". + * This verifies that the method correctly provides the description for the JPEG filter. + */ + @Test + public void test_getDescription_1() { + JPEGFilter filter = new JPEGFilter(); + String description = filter.getDescription(); + assertEquals("Generated Thumbnail", description); + } + + /** + * Tests that getFilteredName method appends ".jpg" to the input filename. + */ + @Test + public void test_getFilteredName_appendsJpgExtension() { + JPEGFilter filter = new JPEGFilter(); + String oldFilename = "testimage"; + String expectedResult = "testimage.jpg"; + String actualResult = filter.getFilteredName(oldFilename); + assertEquals(expectedResult, actualResult); + } + + /** + * Test case for getFormatString method of JPEGFilter class. + * Verifies that the method returns the expected string "JPEG". + */ + @Test + public void test_getFormatString_returnsJPEG() { + JPEGFilter filter = new JPEGFilter(); + String result = filter.getFormatString(); + assertEquals("JPEG", result); + } + + /** + * Tests the behavior of getImageRotationUsingImageReader when an ImageProcessingException occurs. + * This test verifies that the method handles an ImageProcessingException by logging the error + * and returning 0 degrees rotation. + */ + @Test + public void test_getImageRotationUsingImageReader_imageProcessingException() { + InputStream errorStream = new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("Simulated image processing error"); + } + }; + int result = JPEGFilter.getImageRotationUsingImageReader(errorStream); + assertEquals(0, result); + } + + /** + * Testcase for getImageRotationUsingImageReader when the image doesn't contain orientation metadata. + * This test verifies that the method returns 0 when there's no ExifIFD0Directory + * or when it doesn't contain the TAG_ORIENTATION. + */ + @Test + public void test_getImageRotationUsingImageReader_noOrientationMetadata() throws IOException { + URL resource = this.getClass().getResource("cat.jpg"); + int rotationAngle = -1; + try (InputStream inputStream = new FileInputStream(resource.getFile())) { + // Call the method under test + rotationAngle = JPEGFilter.getImageRotationUsingImageReader(inputStream); + } + assertEquals(0, rotationAngle); + } + + /** + * Tests the getImageRotationUsingImageReader method when the image contains + * valid EXIF orientation metadata. + * + * This test verifies that the method correctly reads the orientation tag + * from the EXIF metadata and returns the appropriate rotation angle in degrees. + */ + @Test + public void test_getImageRotationUsingImageReader_withValidExifOrientation() throws Exception { + // Create a mock InputStream with EXIF metadata containing orientation information + URL resource = this.getClass().getResource("cat-rotated-90.jpg"); + int rotationAngle = -1; + try (InputStream inputStream = new FileInputStream(resource.getFile())) { + // Call the method under test + rotationAngle = JPEGFilter.getImageRotationUsingImageReader(inputStream); + } + + // Assert the expected rotation angle + // Note: The expected value should be adjusted based on the mock data + assertEquals(90, rotationAngle); + } + + /** + * Tests the getScaledInstance method of JPEGFilter class with higher quality scaling. + * This test verifies that the method correctly scales down an image in multiple passes + * when higherQuality is true and the image dimensions are larger than the target dimensions. + */ + @Test + public void test_getScaledInstance() { + JPEGFilter filter = new JPEGFilter(); + BufferedImage originalImage = new BufferedImage(400, 300, BufferedImage.TYPE_INT_RGB); + int targetWidth = 100; + int targetHeight = 75; + Object hint = RenderingHints.VALUE_INTERPOLATION_BILINEAR; + boolean higherQuality = true; + + BufferedImage result = filter.getScaledInstance(originalImage, targetWidth, targetHeight, hint, higherQuality); + + assertNotNull(result); + assertEquals(targetWidth, result.getWidth()); + assertEquals(targetHeight, result.getHeight()); + } + + /** + * Tests the rotateImage method with a non-zero angle. + * This test verifies that the image is rotated correctly when given a non-zero angle. + */ + @Test + public void test_rotateImage_nonZeroAngle() { + BufferedImage originalImage = new BufferedImage(100, 50, BufferedImage.TYPE_INT_RGB); + int angle = 90; + + BufferedImage rotatedImage = JPEGFilter.rotateImage(originalImage, angle); + + assertNotNull(rotatedImage); + assertEquals(50, rotatedImage.getWidth()); + assertEquals(100, rotatedImage.getHeight()); + } + +} diff --git a/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg b/dspace-api/src/test/resources/org/dspace/app/mediafilter/cat-rotated-90.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5c0f91c4eda737b23e82a092905cacbf9dc56f29 GIT binary patch literal 36813 zcmeFZXIKAj+Ii}DVLV}iR=yZK2Iq_M6V$U&5x78g%*e>h!n})xWfvz43kxUd#=^Ok zh3$VLfcOkxqXQ5?IweF1pkRYgvO$QwU?Ulbbr3DEmedgP1y-1JQ34PODry>9I(i01 z$lr+&fP!*|xC?+#K`1GxD5z;^=%^`~*N!f7RB@;6(;rX?K&|SJ?MQ7hX0K2ImdMu4P7) zUaxBzTF5GU)jGUr?;UV4DZBi2+sKl#k%LcQpIb`G(Z{$!j)VsE2w zPg|v^<>8jo_iXtZ1DP2M0@489>WZA{i!yVETWfMYgz=WpHigE2&5C;!=fZ!c+P~MJ zsEvc~aJUfb?I=uX!rfTqUox2$&e^9cw`gOG73yVHBJ!Sg6 z-_NWEQp)YufBNFX)f~@~keRmknpZtu?e7eJMjLkf9?dhO;uQzA&bapQc4;DTR{IU} zQGK&m2&{X(x5_>Jp^C~Gh3O}oF^io~^^V+hx_O>{dCfNzwdg!LKdwfw4MB(h&>G&1 z#iRTZA+7uOOX`#zr4fzF7KFe>^fzsxFdx05b9_B*Y_;)RuA_=^eV;NmYkOh~kU+Bs zU>V*JIsUlo1j}bNORdQ#4uxw55%uM1QJX_X&Jt9mOKqCQ9dzNu&!ZtXD%jNzQ_eU^ z^0yYXmZryxMkK66t9*Ma*{gZy^*dNdeQOf;!U+#H!L;h@!zz?L{pS`xo|#p3l@EfP zqfJZ{%~*scO*K8ki-+Ff)nU(cZa>NK68b^4(elV*MP~br3pnb~Uekafdv5`4wV3kw z*_&yf?AGI*us8c1Lq_$~WaU^a4%1c$h%FQdRJK*&Q28sGDXBNsc^5{frv$DCW7_yr zdzh+bBaRgC6!6;B&}dCfq*25V!EaKlzr0 z)a-Ly=kdO2AoTVptsFIkYu%wm(@Xv>DO%^Q(tKZ)o!7d%X!y1J_}vSK(~ci6(Ud9d z`7Ay-_Tp2?>&JDz*$QpPr?`@)csaECZt@*s@k18(g)9?+w+DI8$Di4Ipr(0M!_-Ji zdVLtsbt5*8R=$mXvuUI4bZkoCKvByiVH9;L@-1yC>qpzuudaW}#tRv(ZXDQaoNYoG zs$3ZgoBAPbJ&t{^s@>eN`xbsl#rd-itq!g3Q#Be5-mYF_-{zsLxcAuJme;sgEWwKU z4p*0_8mu7rJ4YSgBxoLIU&GV@XPkf4UxWCvb zs%ZEQ0bgeG>z!jpbTw~=G`$-qG#SJ~@0_5Phz%$$Wh^{J1Tq$CkMQyB-cuejkoAcW z@$#!SI;|7kayqNUf3CP-F{L65ulN>x3yTcj;H*O;nMzHjWX&t_!f0q9lSMA zkpEdl0n0~o|N$Yc%~bjMr<1DM|656 zdVLo&0(E&UyQhlBU1du3jK$S@eA}vn{oYaEwMU!GcK%d=VU1gKPTldi$SLGS z1!O>z*p|pn zZ{Ky)@`=$SLpQ2=zM2G#H-79HW|$!;m0p#UVYIksSbeZ#Bs5U+dA+FbcQ9hFv+; z@ut|UGE|ypL2d)hRoX*4lQ4}=ZQ1ZlpK!V|AeniJ=}p(!06kUo9)#P(8nqqA!ksbc zTJd+`1y!E!$2T0kdY8=RCp@lDp4jA_Td@2NPP$fSjs3pWyi3{bIw660dApsxZ8iMy z>nKd4v*MBE+7|DLJ*DTqe^tJ9WyW;u8)jxh&B3UpGA!wsLZ$1=0~d@n`Rr-$LS21& zf>l4JDP&11FkB{7xhAJMNz}sAGTd)!wa4bQvLN2Jrxg_3b*BjRf z1^#^Edm|}x7Ee6glkMEx#o6;vGr#+&#|WLeVojbJUDNXwIMl#sdZE>B==e-h{>^e{~Pv*S)K>Uy6D< zXi45hTdZ%?Vx+kChuHVSmKW?3d!M}_SV{zzKcOyX=lwiG@wFl(Vxc{nvS*~%U8w3M zeaDYYLYfgSToI>uW_dMU?ah~~W%m?2T{{*gpy8%rMBvGK`>KKWowgz6rbi1G!q{gr zq04$Z(r(`8V((^bYfn0<;UD;Uy^Gp3S#K2`X=&GUd0gdMKdYyIvj2hA>(x*_ zogucIrRzQFr)=DV-K9oC;V<6oe)9VG{+y~H=U$cgOtWkH$on@kUQz&iuU9swL$D46 z`!B^2l|56A9a1t!0$y(%Ku}KFh2C;Ct9d7KQ6coDVdUxFGWiwPoK-c>xw{!`kMbK; zL+Ov5N}AZuNSWt8qN0@Z+>g2{!=-rv+TGo`nQ)>cQAa-KHntnS%S#9qjEv(@i0!)9 z_5(hvYrsteh%NKwH7B%}0d+QSCz0o0mIuf3&Ybz8)8>^HU8WaktKG&@yK-Yx8GGj9 zo5Rz5d|kn^T3YLg0o4gbEb8FQYr0}=?})#YsMv0!)oP7!qtQ}TsxM9Wq$Sd2{^%m| zKppdIS71naKCr5*zlR89pM`6F@phxGGknzTcJI@@LMKNBm9wRe15zo*-&)P5dUU)W z@i{AsxvkBN|POJjHB1I~ALVWpq&DApzTuq1f2?AUHxV55EWSb6T&>nX&ihA3a;OSK!zgtyaHC zfvl;_wT-y-HKh3%&9Ll@R`vrFG%97^LQlg+?dXPrw`#H!=b1(?bWi7bU~GJ2W}R6| z($2hVx~!9FQMPAa{`q$h8KBpugm-z>KJ;nro8ue8d1_PP(EgfaHO&`p$IrGtoa)c$ zUN6di^4`M~k^Abxlfu?Q{E_rE!*&KcV_EiGWY@5)rsl^d%(JV&*7e^*9lj)v}zo zmFv8ts3AOo0-g)&5rNa!y`yQqnC-4IKfZo!Y#|IM;PZtDWW}$#-6?w1yfC>5O<0?o zH$Tzd@Dg|4#D9OyC}eOx4R@XK;WYE&;rq$FUOcy^#^;`N-#C{2e7(PEMwSSq2(@c| z%IXtRA_6dGlncL&K=!Efa=W40QZ+5bCghgdgQX48e7$s^F+>CH6S1t z>$Ob$uV|ahm*n<7U-jQh1jOcBL!sZM(=3$ZG$S_zLeROGD(f#xH$qTXgnU*cA$Du7 zj2a82N9JhC&k<&q4>4DDR^Z3^F_pCvZR^C@r5d*+DF2D+hxADYakYo-MG#&;+6Hp`q2_`-DkXTZhm7LcG9z^2Mh2u$i zNaQV0vw5lqsbycIz5?N&)ms$OB-q=5fqZ(?N^K0e`~R& zA0CcD<3Rdu5~rRY9=+2Du#ukFp9yOpocYg$9u}?lD}l!UN|<>faX%>*KKOv22?G>T z|7QZ!<1f*=P=wF6BDS;ySR3i;fwiF|Rms{*P|yoQ@YKe9AtlL|5f=RqI9=pF^66r| zZ0*o^Lt$$qsc(N?x=1g01S%9VD&xl9;6nKD{19ph1;$SpQLI7W-#w9F@G!87W8lavtn(a$Su4T7OL$;l0d$8L9rM=2VWT2xuAu@(LOus zV<8c&!8++;@OTUgi9!2pXJOi*kgD(-o_Pmv4fpZ=oxr$50PFUXpEOn|r?%>_?em1} zJjvroluTpUE);xdodLG&jhT!id3^cFoO}M7rkp3I>B)ES1|jDHy}_i5VSA8(XmSk} z$yj4R+S?nXAi+E6iv};qHs^i z|Fj&41em13A>mLM&K_w-vVF*3%AqFHKr}n}M@vqa=>Jw0tq&GM@cRpj27`tBz<*9_ z1~w$=mSh|#gV}?C#~8uTFsuh2<^>`E+hD()S(#yrnS>!xOi(_8;N$~^z<`G*=&%$7!ympNj0gX4WC(Q70v!K=LlDpdLnTTifn?7RYdQ!5G~6hRAIaHI z@{p1WB3-oL-xdtscf7mXI0z|CV*2eO^X}lOc7T8NBxNK?`1AKi2Y+nf|4keCee$>l zCVoY1SZqD8J}^8O?j?orNU;)f^vfKN*ZWJ%gu%iC4-bGHBY2SF8|2U@DWtg_H6hPX zq_F4jX;NtP_cR#HMeT$SfMZB}@HUY&oiTVK30NHz7!$&iqNyNe=VFDSJYilYdU{|C z*&B|8?L-+FNC`s>76pb}w!+^GhCeZ+%tx&ZjKBvI8e{H($HT}0f}PA@1RUdoh2d~u zlXh@SAz;+N3v3_~jt~Az+v%N26ce;J29!q%PSHbsJdKPj@v@r#$N0bL{k_FJcL{*7#e1;bXq(b@k-w}yHBM*Vwk8$8$< z+jLT|{#LjN%EJd{<$?G8OT38F4hJJsba1~4+EE=7sj2_Af!tsjTsp`=k6@gRmzNF_ zuY<>9;hqFMxl>6sVE&2G1EZ1KRU=t1lO7U-gXw^wEf1fceL`n|hGM+Haj?w@@&W85 ztFrM30I~nJE({d_jDQ`BB598K7pu7kbf>deeqrq_OdNO6zscR+HASM3ZDVUqi#Zqxg!_Rb z8Pv?re&E^C_rKHz{dVgi+fUBz=Z0A>+7SV4i`)^tcRT$L zqCt^tJ68Qu_dAUyQOv;g5C-6q95jx!v}H$8yNobq;AFJZ1;6IGUF2Rg_wX~rfEEO1 z*)Ev0osDUSMv91T<3TjQYJ!F$30@>=D3THF6wkhmcZOlXNf8SMw11LlOwe8+D@n-q ze8y-7#=8yBq~UE~$sDVNR0&jSj>;tWuJ_+ULS zl)NL0ly8R!hKJ$c?2Gq6ZWlzF=~%aOl7hkD;Q5`zWCOlDV7RT>|F?EC+v+>Oz3{%q zu;12^kc^EM9OZwT0BFFj(6h1G(LZS>G6Ne=YSz!*+0h%fISlXN1+K5zYWu&P2S^Jd z7{S>{kAMSLD41C9$e;yr2973%J82p)?PzYbQ$EL5(gs7oLdk={&e~*$#z>yItuWvm z^7}iJWv5nup^*BB)d35~!_47mILZULT?l!OA>%=WEyf1!1%rBE|1#`oK~P#fdawR94MSZAo!#p|61BwvqB5L48d)95P&$qCgh99`>9Gxqj6Fmq&<^TPz*{s z$iq)sR!T-1P}d3q%>V(z3wpvptJ4sgsj3wcgnMZSIVhUTnEUC$eBs6+SeQ+Sg)KA$ z0X^a+q@^jS9;6zC@;55P5H-AlNoRJq#8m zs3;{b36+tR6;x7^l2uStQj(Pr1h;F-Ny~u$l_bG^n~F-Ra>|0+mk?MQ*2`PfTHkQH zEHI_9)2P6}K&e1^DGb&}TK341BP1Dea*`m2BrX`V+8{|ZPIyOxJ`4xN!pZB_1W6LX zHCY6_h7efOttO!Sev194y4qp%g8t<51J`np3-^Lb!;moYkOuWCOKLt@sNc^1)Sp@Y zBKjwp+!;HWK-HR?|MLtx{f0vQECPo&2mqV$?|K;+ZzCV!P`2!W%|2PS>e z59T8z__GgG$%7dz8LXcC@AzMzxEew@aEYOyiGiMy>=7j;NjWK5a(&IsRZY-1yayTz zGtt)&0vjs@hkL2Y8!G6@=&R`IACc2FP>_|?QPNc~R5ma)P?1&ESJKnn$*Yfn5=geQ zlh=!s_sIV&@77W$59EI(gEW#!#-?fv2j@g=@b*}u>4Hkfdz#TR`=nj^_V?J%O(TRj>bQ4%;esYYFFnIfs$# zf!x^N-tRjo@S}kQ{`^|e-1(~b^ZcWMKN|R>fj=7fqk%sf_W za2*Io1rSJkV<^o)_+kJY26~eyTR>O@NkG9tn6&qa69w}C9n$1o6C!vR)E9(hKs*!H z)>;pQ)j>xTlh1Fk=Wj3`47-550LckY-nJol5PC=ubl9i}8p8sSV1!H33UoqwV7)*W z5$Neg2Lr%Xn@PC<_J8=%O-66s{w?u0zJCXj`gV&xe~jE`5bpQCaKCc?!eKz)n+mwk zjPlAaoaZe7sE7ano~~awk#x}KcL4y(KmFDoZj!xh`)f!JEI14*MN;U`>pwF5Fane( zQb7vjXTO=z+k;C>0Kh?Dbj$8R7^IW$f4Nb_gGmY;oV;@k(6hD`gc7g;TP#$h2Q|1m zkrm(s_JAkc2?GZKNk9%z2Gjs;zyLT3SOT_y6W|I!!Nn^maQ|H(a0)mJTm)i(E5LQ& zHjoBn0=YmTPy$o{)xdM$HSiAj4738>z!zW`m;`2lC14Fahm97(4B>$6fe1hjKn_FX zASw_|hylb5VhwSExI=s(Xb1sv3UVG214)G3hTMhZLW&_zAkQIhA&rnu$N*#<@*T2D zK}o?x!AY@?LX<+9LWM$y!j!_6!iBS5|78X6jI8ZjDW8eOshd_ zN$Ww2r@csfi}nF+Eo~F+FzpXIMmhmHIXXi+XF4?9Il3geJh~dXCc06&HTqrj2k2Gl zE$O}JgX!bxGwCboKhY1-b^V2WePVS2{Y$u!T*%q+^R&FsvKXO3geWq!%r$NYnZ zgGGwPgvE;`oF$p1oTZ6nnw6gQ0ILqGD{BaA66+(@Ppsc|(eD!7rMJst*Xdo!yPoW7 z-?hlb!6wUQ$%bOP%$CRYmTiokmR*$HfZdDz0(%DgOZGtyN)BNTJq{?xd5%nuR~*Bf zG@N3bCY&cZV>k;qKXT4;v2!VK*>eSPC38LH`oc}gEy``ejpUB!F5zzDUfsQKx6W?Z z?n}E1cQ@@`=HcVf=7I4<^Az#4@~rL=++(l@u_s|q#hzYXDqeBkW4wX9sl2avzwvSM zsqsPiqWMbry7p4+72j*UH*{~t-uHVK_wnyD+UK|L*1nhfzVUPOYxAGvzs6s~KQ6!_ zpeXdMbQ87^m(MZt>(J?U|F%z*6v3#+fgUkoD z4*DOwcd+FU%^{UTh(jrdK8sU`D~kJyCyReNOmSH0F#K@J;YJB+i6atdiF*?5l1!31 zk^z$Wk^@rQQf5-;q$;GQrA4Hjr4yvF;Rp?OUP&8MJRD7Yhp`@&YRVq{(R~AurSH7d%uEMEerE*#2 z%@OJ&x<|r~R3BMURZ_*PmZ;9CNva{#^3=xE#nfTynd*ZYLK+?#cQw9f3TnD*rfYuD z+OOrIbx-T7wy?IBcDDAY&LN$XIuCTFb!Bw1x@Ee{da8P1de8JJ^^NqS^*J+ooU5#Lci~ zmF5)YN6oLA_gEaTKwDH;0+uG0S1o(3#H{?Ss*cedvpSZ1Y{XjD`jmCO4Tp`JO`gq? zt*&jnZMWS)JA&N{dsh1s_PO@U4*Cv>4qqIl9m5>oIq^EdogO>WI@>#EJ1-tLIDY;3 z@Cn5e7f-agh`9v0ymjSuMY=w9V|DXzd*n{-?%A--gefG(MSjav5QX$UzX14#*-D z9m*Z`1kH{{pzARFm>^7}-(kOteqa1m{jdAaVokBxI0()K_ZZI!eoVb593(^#1_Cq# zk^_DO+6O)gVhchCy$==-z7#wfVi=MYN)-wXeQ|34sk5gB!gRv!o`#(EJpCeEIQ&BR z@EM~s_s=q%MV$R`PUc+VxrOr%=PNJlyKv^hV1!Xb{zaCH*o*CvYLRJCR8jD#kC)^x z-MX|H?G@b+BNLMpvwj(Rxgl0I_GTg=+53dm+ma5c%?L@ zYNr;a?MsVETTO?jcic6)TYgXUUQz~4MnJ|$rhR68mP%G`Hg9%J_C^jm=gWQT`*pd> zxw(0J^WyU<@(KCl4^BMzT%cd@xKOh2ZV`7;%tPQI;o(HFdvRNdX~~O6Dvt_F50u_1 z<0y+Rhm;4G&sO+We0l8jxbex+Cod}1D$A;*s&cCLS0_K^dKzEDP;;S%SQ}Ei_{{Iw zx92|32Vc0o=zi(=vZd}=-N#p^uim^ieEqUsr@r=$#+&N5s&Ah(s5Df(Q+ikaUh#d| z2gMI%AC$K}PHx6x7ZklfP5K&;X{@2N_q;G+(lU-@3 zsHmu^XsD@a=xM+|MmicAIz}c221W)3rd>?rYx}X4`1>6KzT=pgn0a>X;`!g4?D_^G zK?wnmLj85Jt0=J!0JlR^P*6gC`_dtuk_w@q1?a%Db4e$@k{YsoLCD9t?gA*l16?V> zCV}U-zqsc8=WL?Fwf%vWw!c z;S276Zo9A1Kks;g+-~U zBrkTE(OSmoL91VCe~9~#NJ}0M&DYGn^u97keoVAqzNyUMD#9%JyuWd086=s3l8L$W z9`InfWnO9Z(AE8^Ei{>mRrjextxcw~E{SX9dMb{7%e$l&eW{}6^=7l31*?kQ%h(^G z;lg)cMTs%g5_COxLCi znZYwn@8Yw{g&jC@>D%MKvj!Ka_%zkz4=Cmt)ZJ+Cvs9&d;5fn07l*%_8KNLLI&q|} zN51Gj5h!QK9Y1BeH=t+DtzijvvA0nIp8y8m+i9MN&mzzj7={;*L5Xy92)SgphLfKTX;OCGOh)Zongn3^hP-CzrHp7dhruMuH9?H$$9$fLf6wQqexct<{>A+A-+23T zc=53YvI5+x{U~8%b4*8QF2y{JVA+y1%AtwfX&1scx#Riu8>Gyiw@;>by-TD* zDHImJ;^`GBe16P`-6{4Am(nFg+vK%Z8>JTm>M>S>CUVoXZ`qA+)E;6x8lG1|1khSZ z&5ixDCHT0Crpnhm@5gV+j(EzJd}8Z~coMS|-JY72x@Ymjje{ShmS>v3Wm)$Wcl-B+ z+sU8AKmC}>hUrbe6{i3jJWcW7l8-Li+r4xsjf>eaj7(3HzY!!fDxLFoN#AkyM*)78 z%^@Ks2XUsamsF>@X`kkX#TM#4yf&ro7&n>wRISm`5})c*hpJf*oq1id)}IhhY|8R1 zsPt8=o3z)^M@Fkny6(Y8#vij!dS89$LODZ_f$;GVc6Qsq=&{9;@rC}3;>%&QT;YJ0 z9iso;5p~h(Y!};>%RV0x6jYm@iOb$h)y9>XpMi2fx;x-Z)*G1dZgDcT)8F^x<2l-7Su2KRP+7ZX)dW-8($}S_YCeMgb#eshQi1>o6kP@VSRJ1+=Quj>F`kUE=61f%0>^-(SwICTO_`OU=NU z01kv6`f@f|knUL>@V)=WC;s1Tm2)fkmxnX^OCK zOBSMSeMNTDL#JjYifmVJw64TTJKTBIe4gV{<2_H=gkTJBa1mxLalN0VYd$P>vj1J{ zV#txf0ge?F>Xxi%f9|peq12_hvC@Yfmfp3l@Fy=CDjk;RztQ62-VlKVkBNMHu$IP1 z_y;1~?a^f5?8x$J4R{ov|AK8q>xYP?ve>G~sUkz8>kpnQwou;fqODmNlH~PdNP}w` za5ffPm2f_vUY@dWsY>2VD%A1xBO7>zvzR>adU-DNahzR_qDoy*0g4EmKmGLeb&B^h zl!-aoXzeA}jISxEOA1$$fOxt@i#~^$oKx5M8p7q#wJe)!sNQ3d@xc+f+#ej!yCrf!pIw2|m#C zP!XoRlsNE{uWU5;G!H>yMM>0Nf|A1a$2B(pPJbO=_93L|c&PuB&>ijwiD;`E?s?Sj zWPt#G1#1cQ4|N&6tg@aG@G-dN2`eu7qxT)hpPuVk8!O-Y-DP#ulqzFHci+)-$!*Ih z?ZLY5xoeg~n-xEFS6{Y=Q8v4X32O-jeV6m}j@(V7H1{r1YifKJJod0RJRzLR=uz*H z+s&=xlT~q)nroNFk|MC3gb*6BMe zSs<+@;0YZPdHmV@qwnCA)ZRh=B1}wm!^)c6Ag*K;ez(2TKrF6FO-In0D(h@##Xh5` zv#YsZ_C&wFjCyGrSZ1+jPpa&p)7mlu2Dw*aBu*bc_#+_CtxMX*SMthR(Ggn1?aN;{D|KiG$f3H@P8jz5I%7t9LgT=H{fkUDRdA7imq-NHK{YIk+Z{Ic*{c zZF=Tr>$AT&o8eF3Gag% zg|c-bzUOFM3lmZew;K9TYI$Aa6-#pOosKn=VN>{9L7u0U@`1PGbcNCL^vc%EE$5eC z0n3&g1hdDjg#n1K%7U_0vSKs5@^81lN@E>UE7m=wqt3VnN#X0Qc2`!+bV56$ z=Jr$CU6%@ffBnI*_z2qc)}FxmABGt{p$GFvGHkKb_ ziVr^U_|`-B`6Gy7hD8mBO|eaN0VENqA_6%F5+W(H@*UrsoiaxAmnSFH(=IhzKQO~0HfjB{LAwaGkcA)&p*@FM#_V_Vw@ydj%k zQFPJz^2sR9+eHtbJ2Ki{K1pAG{wUoZ6=sIX>l)Ni>o_zuJ zRegW;)3OiKw+Nl9y+QTj6HS9}GR0>F0mWKPyAQ25C!&|!EMg8c8I~H#;;+UHFWK8v zyPVVK#;_*}x$sp}=CydQC)7AQ_==n5@OZ`{z%dox-pvl?;alxRjti4F_!Ihi#q!pMalka*D5UH1+*{Uo<^ zND4a71E1$uh)%Y2RCys;m@~k#I8H*=BXp8W8t77w$VNsx_-hav_(guE>%&^pd9*J^jS$t5)9`ojNxB{!{on!a!$P8v*quFEdW76<4E^UU^ z>UrH*eB>3&G*wq>uby(+=&J*l;FZgq) zGs}N7dhBUEwe7PY*-q58mT{yd*2?>s(bva%X^Fd zK=EX|{dYuCMLWFaDY!gPhIfG6UZux*&W2YHC>MvdaX%IrD=pY$5uNpa`Is*M@fbzV z`K(g0$T0THsvWQc4!W;b-}IC=>jq1Q*=F`uzIeuN&-D2`;o7bX$0LgB+Nv9pg|u2+ z5O)b?^2t)_k-4w)3f~M^`+j&)H6_q;9NCOO6|9sUiE>R~T1iOYc?1NU#SzMrmS%G& zO2IFQd#bKpVVIo3$n~lkGs9p#Y9)JS#6Dzj#mf^nKH-Rf^K$rCA}~0dhPpGGmmDio_X2w@OzWO;rLj=^ZPzVnZ_2r zoIJMH5{`&cnQJg&Kt4kEG{#JE1JsBB^uTgHBX5H`ef16V{5Mb5(tA=r3i{>t*vnl~ zk3VE>ex{4Lw4~?zpsBve;C`2e99`Xeai{sNuEE7c>avzzEIU3HFPH|8sO81odnl0_ z8QYSj>0y2L`fbUoMhaFs3&!fSOZiUU-o)n#w}erCPvP>v$H)Vxy4)0$6DaheG3$qP zX}Rf;WA48Tn1? zl+SSGADILXo%ZRkSRW2JIKY>%+kh9+qrf$36l3vxxSs#E8B@sjQ{O3&XMIA08jlTuqUwOU8dIGry=j!dMXRT{9nYhv_BD za6tP@M5sxIPSKlhQk>&{Nvq>mx`#UJU%g)cu_j)cM*N(=Xnvpqw%PDCuJD?~n%1$~ z>-j-%(|OODMbv3~iKRfaF0c$Owc@_3;?wj*r|)t_GK^hDlz!zX3%ho?ZBq5NUmk8> zGi(XilShe}CP|FvrQsu>VzPLG8GL z$&k$;jq!#~+(c$Q6=pY1^+%L?~e0czJOirfiJX;1qLU~bgw#cT2*q{i?;2{={jla!=;{C>5>K1bA;kW zPK$!y_)XbZ9uc{uTMu%*3~1IB2N%mEX{}9!uOcyJyUpLFKeZK?e;F&PK9CSNCHCQ3 z2PInMI}xz4o44_lPk*&io%RlR_w)r??3I82@ncVhdul5jX?doCl@&jBEHa#dqfSHX zKpQPke_VU=8~y;p?m+ja8%src{2^-chdvD2csxs}e}O(&8Y3jHnt#gdncL)p@!O7^ z2L=86EFb!c$U*{d8Z7#6oVf1`L)7OB9y1lEEZ8tqbTYg26DJnCrZB5)yWW5Cj z?Rwuo%$=#Tf+$~B?p`f&4m-s`Y|JCjCyXF^_qrK!(`;OPoE6X~o~w2AHhhTSlE{&x z=rR4n9Hn--_TvRrZ;OoxB~!!&b)M54tGVN02-z=Q&kyu6#HvIq)Rc=p{Cwj$bEx== zrNyl=!0@E(K(`AM;^Cli_A~!)xlF?&&o9=X^)5sZyw$75KNj1hAj6>JV{>LNHQ8h4 zxcrQ32|sMWpEb3giU!z;Ku`Zs#@f-w7s+zDBI9?pUcR>_OpmU>HTH!w?lxJ}GMG5Z z!i}`)IYH%T{`AgCAM17NYNvS)-Jn)Rd1^}Q`iT2=HK#VGz#osrszp{BS^`k_uSe)L z`#ByrmM2=C3D0{R*7p`|gvn|3>i#||@I5H_gg2iwhO5B8NR4;&^(yC&wU9y%$6(pA zHLGQr;i_Sed7F`7?!pbNH}L8YXQ~)`Vp5w6?7R}{Z+~o{^aq2;0vRT_&o z%n$6&8B{t-(cF|Z*dBM&=R=HxG9H|yos+u!av^RUIY zwylZe1nYq*{O zv1z1p!`f5-;#c|X%5P|oD%*{L@9oKm7Ek^xh5Nb%nY<|mH(5@GhBIsMcuGAuR%cz9 z>8jC{&-RAjcJswcD}v*3n;NZa=O`nVqC*^geegd9oer44!b|e3PBr9(N*F92O&#Me z9p9&E1%)2wmraqJ8J;Qd`Sz%`AfLubkmk0I?RA-?6a}ZSllG`T}z3z?h8G% zh1%mV&V@t`ftFyCgew0?A>l8H@)eT;)A@t1%;)zxWQAj&>cuwt)XBeSGsX;@8>4iEoBJIG$wfo9v0lrqKdLt1t+7lY|BeYR!FCS3`7}q zY@~R1yJM&(<6y|u2ZM`29&V#w8X~$5(LOIv*EG>}U+gY1r(+S&5v1xWWCO69= z&d+3qEU1(DTJY_GYHU`^l1Q9O3ci?s?nMssbS>BF@oOUT0QD2TCY6yVy`mI+!h(>Y zvqNt%*_GXoB97Qa!NV-R-%#OMj-c;Xs1a(o%?=qTaQHz4Bxb5Kc~>#5dp4}5QgqKg z%X+pK85(RCs^yBkwZ@(8Gns;*d&cK=Gnh*JQSu{Mk$n||<$bER#&4v!X-=Z4He3(! zHpNS=avehiE;%TlsLV2kQeUAlbf9T}JZx2fZfKm|e{4vLQP`zwCHdGn8`<~vyI3-> zD4jsJ-yAp=({fQH)i&Y5g{pmqx3j7iG7&`JuIhu14Yz%5+EJr*N7z2acOG92yVc?` zy*Q)TyGx|_L5yd?SE|s%9rf{N6_nj+SqQ?^X8PR-y9Sx+4)+srs{xlK{JTbvVrihd z6;#*_f}zK;K>nqtc_HQ|(66s;>Lk2ui-hu2>P@y%Dj%*O_9ddc~k z^xXT;JW>8Ld9`mX2lDp2ah$qWUmb_{ zrD}M1G$VGCqqie8-$Lq9zJpovhiCM=iamk3o(}AE!K#n==%pt0_rA|N818pG`%#24 zsYMy-+N&1j6wlAPu^yl_9JHxF_CA2^C?Uf8$)?fLXyM>T8`c9NQR|{sdIvWMMSb=T z5$~G!9aweKuCEJZz2-E0JrYsD7vN}12q?tGC`q991|Cm7X&ZnmJ9>FoTkhd)K_qkR z)XU!2*CW-Bq!khYd3H^8BV14*szvg3mSv|Azxd3{RL|jTranVY9gcwpKSE%5En5 zf6+vpR9gyqGrqVw|G~t^ec)*2y0$;}(h1(t9=tnG)2DNGftVEw}UJyLf zDfjz>Q)Lsrm-ZeJzOrkxjW59OTW+!6H@`QrBNL5p?<%*jbz{11Y)e7{ye~DjB;>_U#$h;~_-<0uh+w7Xy72m5Jrq8~rGRu-Sh_Joiv(vO9#8l5l)B|GHwlw6#%ru0orVd;D{;=A$pzk#D|LnZF&F&VopP5Rh7X zGCTcr?a;Uuv1MfDG8OVnWV|{dFK+jFY0=Uy+GA2$`FCvlc&v-Ie4s* z1)<7p&^}G)s}SYKTnx0MdQ5Pvs*<-6fqo`f>0X)odKEH|^k`wN}w)s8JG zx-qE~+2`-sqAZ@?OMkOinA==w&p7I}uiM~m%t*o~Dt;mmwO;zAPFer$t&l?#DUM3( z?GnXo6)$n6kU2T$TiGEq*6V3zPWQs4u<5T0Pz@|Mil^^2B~Zq0JooK8Cz$SCWIjR{{@5^d*#pn0FZe+(<!eo1Ff{soBq$& zNZWZ46?KKyb%CUNQ$B`fb6cBhn?7d~vNctz!s#Bz6DYlWI&}m;m>Y2p<0LO>y zyWL2s8vDN5(lf0eM*3){%fnObBkEdkt3mNG{Ww(BKxJ|p-YTH1r00RAiyP?o_kDFb zPb_4U*azEDK%Pg-_&;4Qu*d(^_2d#*E!Dmd)U<|+%~^Cl0yQn! z!(Q8V(yE4ym4jb(t_k2f#Y_zuk=2R|72=oFAH1i9nx0`0Iddh;0#ZMTd5Y8Psn(4o z4C=vwE2@#D_XFWHNpMgmR1#ga+_xUyVy~K-k@Uw>iyn0n5KNjc5O3ecw#HZGd&cVu z#YW-*(!*b-^)>~M8!8oy65_;O`j57v!&1FLk(25kvx$bI%Ed`8#k3+cH+OFPd?JXf z#mr=4zmp>(w0NopC_fUY76<66#HhZg>oaC$&hi3?tYeKh5Uap@4M*j2cvQ@}88Q)Y zQ9c9NZXo+PRs1E?eG%#flO7(KGPdZ``BLkugY#dQzaP6(MlLTJ$rD7tGB_ZEz=KK> zmDhbc6fR{*!GP$&wYSrHc&XC2E)*#?#ba;#WSunmZ_B!(#*(tQM*470DZzmpM}0}@ z?w``;bG-+FZ@3%$J=C!DSuo>MxPtMW0Xh-%)4;=GO0p`3cZ}E-RsarQ=|L=g8`D`b zGL!{_U*Z6e4{o%qx8YLbr21ph@Z%MhXvdgsaba8fy&sy#+wc5GQR!=JRR9YM0bZQ= zdDrCc0s3;IPZ|-%{{W@v5rsOF-cQwPdR8^zbKCnVDioOA{)(top*A{j)|y3vT`$}Z z)kYD7b+JE$R(%tHIe#(TYgAx=|JLG2R6T+48?=1NH$6G4c2|+FRP-{k#L-9t#OriF z6_@`2gZ;DuTUHqgja9T=bR(9uu-PD}+B+_n=SvS;MKL<1v={BM_I%W=61GBLBXMiF zxgCeJYfGbp%5=TRwT-=+YAQ@Hs2OJ2v9(s?w5j9tRm=EJ0_vV&Rv@dJt8(G>Rd0eT zHarfJsuCpdl_TN>d#dMGO9^!ee78%I0|Cd5yl+?Q=Tp^Z9zz&O7Tc8gNxviARWFqF zJT4eHtt@fM9aDj~%}&O0t~e6Qmk=jg0l28tdv8%i&u}GI8_prjRf>v$CjCd_=ndRac#5o>g8w z3p)m1%g-A|<5h8}V0nvCQ!0Pc{{WiC%1I6^l0|WI8WsSJH*prJ!06c7auI0-)c0De z5Z@vgvLj%4o+8y!61SJ>{5NyC0qhpx>^+pev0rM&H$%i%-Nm>vo_w{Xc!&3?n6a^= zj0o3a5vb?%(UKS0;nVTld2z1LbR_GiWf*^B*dCh2EV}3ptDzr7ALc*Nap~BRAXQZW z4FLkT=ayY`v0^QNvEf@t%c`|tZSogF4wbtjC8--yc+eULQOAu2^P7We0a9iL+9E*r53+(3I{RK>s7~)NirtKg$wZI45bTm*D!V(+>D92JR zHiK0(sZ6P7bq^ARxU(@JSg&W^tL9INn^DdD)mL{1VcZvp97(yYzC0l05y>EFtyXs=GP|3k)9dg3o8b-0_wfhgZ zm2EoMBE!-8R!>Pp&c=nO$|C#7-LM^PwD9*-_0^$d>HTZe8F{c7pko%_<~Gu!Z)oqV z{EUly8Cb*00vVXE!*OCgwRn`K9FZ9b3hBH508sc(vWx>}WJMMCkgNs4ULQpm5l=Um zOQVJ#dei7>-Z)bv$4;A3uAP-os>{>Z z=x@iew&pXnJ%@L7QN*4Vw=tcoRx@pkC?>+%8r23PHvsn&NNdCbZ9vwC8s_xVtQY_k zDpKyYZrx}-1=jxnL8eX8Gh^LqR*|&E48Gt#y3W#L@`H8z_|)k?|IpylIc5rqcPTcv zXD~af%aQ5pc;`^Tf$%NE^>9_Amej5u0tv5@+Zc=H3_ zMDiM$1C6VwOFc$BM?kYkZZZo>8sdNvBWBV%iuKYMZTPzr`e-pYHs0<@}2t#ETX(%><1WG#C8e zP`23{{{WW&KhoX)f0@4JUbC?&)4Hohh|jVNM;F7vQK*jg^B?z{;J=Pm@q9HMaw1nD zu_w0ZR0ZPX_=z3$o+?l>q`*vh5kONVuA=7TaN|#ydf%n>mXE_YF|oK0d#?=*RfMqO z^^_{4O?xewK^ph_MNSe=8>s?8X`0LNY5=!=JO&%o5X4)^{{YBs50*){xYD*mlDwTt zY^_zwF#iCy;9OGCf2p2!CbN{RW8!to<=86|ZC1Yi@Z*Rb<3*VbATYWMyf|`s#Gxq$is(w%GX&_CF}^ zttW(hkAv%Mr^*ns#_Yp+f;~uI-r-Mtor`}@>v&lHPRgyvQy{@lrXR*Zrmop=PgsCFni`a@# zFwE|ONwkB+yL>13PXkQei+-*K#XVf&BMuh(WHEuYAP;6vj z+>M&AXMHPG9*YE@5^(^Hd+B|T5_plkWCv10@dsW%v{L&~kjDQ2C;DKrk##4HA5+$2 z>oUgSw%M=4k@p&SksqneJ8lZ0Nwkr$xb8ix{4`oIBZ)l0Y`U$!0(X5KqN*mHff4jW zZhl$ef5LfHVQNzb83Dg8e63YEO{-1=8vsuMTXPxIyg5@D;92z>D?4_zlNp0G`dBFpi<{*uNqEa2Hg@6WFdJAW3O)vD?wi%R zlpthvIp>_a10E8wypu8TFNcEC=HHrqN%&f^dj9}W#m0(O1I%ah8DR}9Xv1D#H+U7V z+rvMvmFLrc+ygl^W%rr%XQdX*hx_)Zz#mGs!l#7pbQ#=Ex zvVccB)yPPYgYLaKK>L@< z-}4O`=K6lF>W|5Trr$K(q9;pSj=(|tYJic+DyakqMz{fV*N_8=rDmHFZdk&p-dOWn zYuUJWbEgWACN@{)WAf9>iL{Q##LP~n%zhI~@(JW3-Z)kFECiWk;ylLCkfT;(ZLzQ6 z{p#DFD5YGh#t~e`ft-XkJ;j*(&fhWr04-Lh4fMFcK*CZ5_Msqf;>S+qsq5ua;jU)g z$fE2N0^h~m?Ee6&jncrLCEF&(Cc{>>h`S5ki9opL$(-s~vs?mn3N$yTYH@1BG0hOL z)L0(vD>`P^8*R#s2Oi2mWk-)`3NO!2R340X?cwoh?w~dhu^OI#Md;AkdRTkidk?;W zrW2zAbkLD+Xs3fu!PAUtHL(?PCeyINw~ekoZ`)N8lf;!(u167L`WmT9q@u_w2Hh>f zt5bmWexa1TJ;pd~$4NHm!B0CHud!mH*;zs-jz0#?T-+))?ko6Q^Z9Bo!$^OobuOB? zEhkW86j8R{E)#9-_nQ9z4XJZvo<-E}PV2bQV~BCk#}cS@^Me)u>P&~3*OgTEXTg0E zG4;hsRpOEzj-z~=f})`3V`gT*1*i8fs#m1-E%K9>I!})w-2{r4TX1WG;u?HZzW)H7 zJHlXfX*xbsdDxLWhj0>kYyyCO6T{)B*LmeLo$*~Gr*#Z|tB?7JmA*~Hj}6G+8-rV& zZ_l!IJTU8=DUyXH_LKJ2R2G)ybp17@p|lAw`zk0K^7CJRy59Pb|I_)hXL9TtQ^?mZ zG2cgqNTiHAAXi(E$L|h0_taTs8gB+-*8xI}L0(ooKRr`$QXG3723DG?rs*Ue3d60;F$r?C6!1QD}RWw=Og!g zKMhw?;JLm@I2Id{&Ikz|_!4d|YT|FgTce^k<^c(+HMa5KS3%2-XWz=BNt81&J_aP) zQLenrM5gkxd2E5Vn+5<2XbsPczp|*F2sZ`VrHLRfZoEkxh3L{u^gNi5x{)3eb4G*B)vdU66?qHSwcy1s!$M zl|QtlmK^Axl!gb#iaAe^NCO}SQ+^`i$Avkc1$8YJFISNn@FhuO1bJ|l*y^W>l5{rj z_?DcOd=}5j>FEBraiDBGwEVbMvm40BIuMQQvJVK8_DHl`o``?zwQ*Aa@Q`Dld^VOgtVN{HN{V!kpx2 zaow6k04wQoEvK;dQ=m+3-c*gdd||ALKo{*Dzq?DXkqzOL0n=bx8=yM)`2}7Vxzx2P z4$Zx=)JGFAh4!O4V5UF_@Tt|#;!QHtWRb@dW;R>xC5tw`A;-cr)9%yLAuKW~w8fYc z>i)r86N)>7nPY?rh0s!WA@bKG}I!59161M z`M^d`j9TE3Z~hb&O|~dt3Tk_K)Q7y8A_Blzeebr74TJb>PNdjS1{ncv4-v$6Yigh( zOBr7Y)LT>EOq$CgY1*elMu)ujAb79c`zh&Qk-gh!B(dS!dNdl72Oz5NWnISG*wLc| zYzZ;qEh{XG*r#7@Keh=OhZ$lpgmZ{opDAs*P4!~d=Tmz8szi~WCIhlfk~DrLvD$VY zyMGR}{?xIwC36ep#0t$2xEBk&0r3?YZvOyvR*W^#8>h(fN>s#L1|`Ro*0*Ka{6Wy$^wyY3-~k(?nC{B*d_qD5ZQXypS@r#y ztmc_w1%X@hQ@Yr`es`7QS=CBzByCGD-EDO@@Slpav&&5^(-)1J_a#VegmE3Z zbD~1Dcx8z~vW4IQdtX7u!v6s3qJwcfOnfV|b&9bTA5f{LmFeCF=d{@0;a$Cg+U=9@`pN9-9Ur1ECJYi&~xnQuO?_x6h_i~~JCC^aNH<|#JJ0aleNjpFz#eJffO-F?gf)kPAX1wn9x|JGiBY`dcnsU-j z^h{xm{L;lSr~aHI_(3N{7vWk@{{RtPhaHP!#Le*(QhlUXWN}d`ho}!wAOF_*p{?OO zE^Ne`*k7`_NMi5%G+q_`4iz{#q=*y$br8!!YqY11a^*mwXF6i1L!`x z7n>roJO_fPvtP7Puu9vfdMB*Tt7T=79uks8D`CZ!FGyo!Lm10;X~w5+fc+IVnrOlTkgK{=Pq8w|;q=Bma+6=x9PE6R@IeBc0)0tQ< zGU?*Bja3ql^rusN?1$+=O0F|+Eo}=P&vg{j$k`F($Bc2;WViB=>JGjy^3iC)vGH*~ z<}x1%NF~5Le5-&o)0OIN@_644X!54Wg`$tlhD*DG8)*l`5r@%FMlEYGNY*&iIF2*s zsp7g^Ui@m1f2T3>)m)gsAR!=Gu0K^@De2XESqG=QW06$-^1U+x&F?&X?hn@i*K0|BK_-L!}ZeG5A@6osn3#$z4f~=Hy!n# zUn+^V^zTq1NQkp2w(bf~?uv@2N?%X)U(5p}8=1}bST(-Iry$g-`fo8d4ZDMCcM?hc zJEO)MIe>75@U7;O48X{U!y{v^?*Pd`ah6@vKfVrdvF(6LZ2 zo-Ul!fI;_CF+bwERIlZXa8y{_>-N$1EET#(MU0YRKpNZlYid5!VY5CKG-`2SP(JV! zS}?4JT!}3k-PqlQNxhHMmm4Lb$kS&5Psl$HRfNGL#N7O#QGlySHdJ@t$A@U5g7ISo zn`F|LBY@*-kjygmM$8KlQmfm>s2a+fGC2W~AkX+s0pT_WQ-s5of+D9Aa+qE(DV`=a1$k42Q1JALv%0gW+}>FP z#@qp?XvMFmV&SGhY}ZpPDnPdZZbFurvJ4l>Y34f~ViqV{iA!;{G9Ugk5?+^*T`v>F z!BBnSs3M%91JSXbEQFDm#gQoT%Ui;G%^;h>X_5gP&XVN+05cvXwXJ`ifP11Wcvun* zyU^brVBEuT+9^Y&;OS7|=Sw~$Eear^8O0obznF0AFTL#4re~4rTz(cumm8)X@G^C1H4a@=!o6Gd4P$Ij*gy= ziJqQ`jgf(ojf<6uiIt0ogM*8MgNKcYd~H9rGJn5Am>C(FS(taRuuc zim?4p7!aQWY;*tu$e@G>0TgTyN;U|w7i=U0u@<5ON=XeNU!cIGixPlPP*Ky+($O<8 zf?fYtBm|(K+(GUFAXE@a3MvX}S{gcPN+tym$wo=FpIwex?}#UdP{0{^8qUN!5A}sb zxNKi|DJWvkHX6{1CfUsn%t3KV+?gfk#0;<4dmFvHi&x&=k!7N#p;jV` zuwRay3X~u~h=W@GOd=;qf)}=pvkD?!1K6`k26kLfoZ_4)4M_l6F{PJHpb&R=dmA|@ z<6&Qaitw)`5dS3tu?t|LB-hCX=l~;AeEumLwvGylQra|*ZSEEHHFJo)^e5salX@Hd zdfF<*tPi!E%CzTe3}R+13`_@jt1EJ+FUZavYJHabA)L2_wka&(Yj*tW_~ZPis{?wC zirYB&4n+vD-ipSQCf z4;xy0$B=Uo_`o)Wkgf@$MV z>tU*zjXZiNX+qauux+rics?S92o&DKGAuIhdAQ4jzI99;^P_ou>9JFbTzae+bxT!y z@#;r(S21SkP#L^p-DATgSm6p(Cy1aNcK-W^!l0&2*Y9_Xq@KLaK!^0!Uf|yJ-z*VL zc#?~NP>9jW>V&Nv)(Lexh1j@}Hu!d7W@YBHq?FLRE>;=ECm9h(@fzzbhn_tPcP~By zTdEf=+od?TQm-&BqjbvnOC28-S}NdcL{QM8P0G=@A+{%R=N^_A-wk@&zg(-e^5alP zRP30R^<ruioZ=Vm2c+Dz zlAe8m>pa>w4TRnLq?4a`QTO7LC7lAH8$_Q2NoMCEBt@ zJ)b28$6kIase4@Om!sHrbc!o^ikCyD?*`vN7JuZ!zR+bN@b&=jxrEbuMV~dVYMGnp z$gB?o`tHQW(aN_mZ#M08U5-o%h!(d@5=K!cqu$b%vVOEb_4?YU9K4Xp>W1iE(;PF( zFqO(M*whah+i~oBb=~HU-8b<|s;-~)X!U6IpK8!(@pko^`ZW(_$G^w+w$$O`uml_G zj}ImR$>zoDnO+YUdsFNr^In%bT0F_(SEkk!xUa<_x0$MTjgP*uNJD&6rltF-kct1D zJs6|r%B5zrhocB*ksIkRLfqTYfjB9^i6b^h4f7nL7#n`U7GO=GtSP~<*w7|FwdtcJn<>o>E2?e zn3C~31bmszzjuxq(bc>e+VpOm&}0+`y?u;YGA^*Rl(Fa}5xBchqsGU#drx`jK=vm> z%c<;^fVqc-i>Vdi7(f5n?mQxpbvJt%HZ+k7lY+NCA9#qQ4!D+)m@YbR)A(HNJ0($hC>B$t1Xf0Eqw2~ z@MiHm5sU3%ff_e1DWxLjVG^{_E)D%>aU zLN!XnUzF#~`OD+scF$G%4V6lVk3c06bvd-L?rU)x9Z&fm!({@x8f7)F^DXMNI{9d! zqFiT=lnZ>^Me|~#)rx(lFqb+r9n<;Q9+m`9~& zGkM$98Lo%(z_~ZpbU&XDog|zZzZ^mY+;1h^Egq}ol10P3pOoBa?Lv>31wAGb*eA}v%_CLOf`W|F+8m*e~`rT z*?UQD?yyK#Y-_tis@_O9?Y)VPt`0ZfE~>SlaDnV^F2~I$Hjled^eC%0#CFZrsmgr| z<~v?-@>Kk%$wR#Y^0(%?snEf|1QpvG2-Gg46XYQ;dUw$8GxYlE!WfEbB4ZnP{ z;t!2Y2W5VU~fK=8=rZ-(@0u9vBdl2puYt#-L3%AE) zY9!u86jph?AK!5H?p?B&pYXg)d2Ew+Zo&FHIO*D)G4=oU>|N?^w+Ttat6S~tZL1NF z>!LA@u1adlH7!09drHrK|EhBH@{IY|H_Xh2hLcH4Wq9%t#Y(qVqUTMu`5bBQK;3+M zLexK|D`rb6GF&25xuvALNY=p9?|R(OX^+cqWkI}cPc_vVw9K!POGUWVD<6HI{z0eB z?k?Lk?zrr(aa!c#Q>&LQ*c_jZOZ3r(SqH}Fz35tM-@Lo*l)a%`R;wq-uGyZ7ce{R# zP#C}`u{Vk`ck$TMJvpw;U7S4+vBU`?`0P_DfSw z2QMic*A?#@wHkR?^F#dmA?x#wNxjeC5UeGG%AZh|v-5tQq4-)68oAIOL)kOZ>mgM2 zioWB=CL!Gf7omhxI=#G_pz-F*m9k8wPPdMQ3220QI1zZV-o9$&bGvOwrRmYa`Ed4` zEaVc2~OHR13956`Y;Cd~&Uj*ya_rQjy39b9?n(u8K7I;BHdlvarS|bf{cBER7UN~L@ zCEdA^Z6~~&YiT#{3-Na|_BAJ5H1PNRz28M|nyt5rjkL5I9DiKpRzGWCc%uKlP2Fmk zf!+{X?$Wg$&69Q>As*5rVepr4c0Z{*x<9un*tJ(RAbA@bh!yRRsKz1J$6Gay(e zg5#Hmk(E7D&K=USYJqhdq6o@Khp?M&7SG;^Tu=;qWgK;?w@hKhEq7IebMDSvwnqhx z>S6RpP9{(6XQa&c7*SQuec?}Cb@zDl0<^okb2IT+Ns^vI?k#LLe3!QnDg+tNp%~Yd z+4ciIt8c_j1c)v3<UNrDo;&s0#M< zg*S($`S`j*)oZsCuTEkz6BcrLz(q94tUum(_HC!--f|=cOmraVGml*BVWGk6Q`omOjGA(FLv9 z0cIECLeBj_Z#Hq~T3sk_qpdI#n3Q;TQ0lXy$(;QINhM9kDIWaHr}&y%-a%UU^_(#uF?G-q3TBp@N zN+5eGYi%QbeGO?jMl&ooqmy$V1&vPKx6sqDQ8T)s=%bz@&3U@f8{N}+4j3EXm|16* zmU6J{nl9^PT9oVAS8(nfL>B0^E8$&Ubqsr2^XBM=aK6S=1hoHIiiY+}_oHW8A58V% z?Ord=dGg-V9Fh0>{F9>ABD`A0nsGa8@2WNtc)wimqg|m1%5P8z*N)3FrCK|&p{Rm+ zT5}`%7L!}rNOJX=`m-kr9z^%vagvjO&cVYkEkB8qzVi6}DIzc`9LjZSB6;dUIa4(z^T&^Y<{o{ov26Mt2#{^Izv*oJ$m0WQ>{V~@5ku;hSRHi zyOYuBp5FB%;RhFT1QyCSgQg?eK+aMYR!0}AO!DdlQi%XV^pVhyX=X2SBK)!U^EY%> z_qn$^a-DgANPS1_1c*~&6TkdM3evKGs=k>el_=Sfv5S2F?MB9_lL5ovorEWOasYtQ z9}7q0tqFL40v^ng99@774sYj$!63;LW@tPNh9;m$2vVB#G(ciWc~){+J9sdOLLZJN zl_8{4p5nv;|us<`lzBtRD83QcZ;8zBX|CO=uLgId6tbFl-KQl%s zq~XsDsK;Nd^`QvgZAENp39vQMHvpxfB#C6}Ehy-XA$aLxzL2uy%LI%52bezcAL;b5 z-u4b?ys@w?lGL|9FMXu9;6H=g;*fYU+$I>QYx|R&ArJ<|W3Yyvcu!Ib$U@rq+TeDq zf|Mo^gHMv!fCbEFi_PDvwFmv1|EyX&C~^yKgN5q4kT{U9T~I8>-^mXKb}ne4aJ27^ z`dCN^TTmu_3?7d`Au(v*?IKJ&7?KFT!I^j9ws2p+-w}*E2vD}4^rW#uIkhFjw$Br? z^CXWWF*1&2yHfC>a~jyPH)b-7Jzs4?rCLl>u3~6Z4 zd3oYszfCdJfCGR7=MF5016%EdA)vi+e;Zg(yc~!l^$$R@#-Cjd=Kp3gfc`JjdPAbO z{b@N6Nia);L&BjjoFmeLWc!f6)I&|ifnavIvVOUQ*%o_v%_96Z|vogaLF$qG#n4x?H!N>n~`zZ-n-HA1qwqV?P0-qGn}ctrNzz+L$?F|3o*SP1{HGd*CeMrt#n(j+_D=v;fC{pb!KMz)*=Yi6PlD#F_yn0UB-;#-HTu zCwWLo6_GAl@NX*(-gmsa+b{?zPa^v5BGc}`sdf_o>Pad{;_&D1j}HFW!2g>z@cZO( z70mpK*s$1pVtrwFFx*QD;gMn`IPf|#8J8D9n zp-5rR-}9u<=gTRX*y%@LK3igC@?03Cq+|1$j-$ILwUix%?u2{ z7_tu>3EPP>GLSOH7%U15xom~M8H|5INQDpE7@2?%CN##<6OV_H0|YyT!3a3U7YoDT zz$Wd$m_os*fj8JdBpe^|m$uWpk}zgy9}LKk6r7@m`gx+!FyvNeLFSp)h=0_?X=;rtug1Pg|(e#5i>4Q~tc{tf&0(sp>T zGq&-hUj40dGnA(<%*GS%_m_APrvnZ~#^~XGRkWizCQ?)XZ3DT%I=J+ZL7pKvJ#TM4 zBwi1X$HKh`cygzbBw+ptF#w~H+oF-Im&pK$!NK&v(3Yp~&px3uLPIg$;5gVO1nB^F zl2zGx27=IkD+>ci%VL1>5BY0DNJ8u}B>02zLQpp(UZjwyKFkl~__O;-MBJu$6mkb| zfI<0#HiyRV1Tbl~C8F4V^8AMgV*@C1CpuN(5x0Dz4o z6{%KmlzYMdVFc`06iIW;zep`Tp*x+$@(b!V04JlJF8DRi?IQQ0rKi6I2DBis z$ack~?QBdtI8sD(8xDd2Rx>mdN$@6dLy?SVr+W5nxGM|`PKsDCp#2j?V}|wyNl8q$ z=QBnNFy3v1CJhJNHg8h*Gj8>p^G-0BR3G~h0tQbSOg~54Prht+{+>`67H5R=$A=hz zq2wK2q;fk*Fgy$gXJ5Q0a=RkZOvk!ik`xRE2hZ;)COh!u0mE(0{=c=G+1}6z?v3{| zh5fdUgk)^A;3)sw1V96Jg@K*jj{ZqAkp-hX*YPGjcXF-pSK|d1p(To%%VpvUV5(7D^rr4z^}HI7af!ZG!>l zkl){#EIU&D1w-m1RwpbR53_`$;V4h!b|vIFh71P*_82?3Hw@~D{mZbU1yxNl%AXn~ z{W4IZ!yoAn-{+{>(4Tbv;93rH<=#*k7!pPv(x6`DNX;iR_21c_`m@Mi zSpP(mJ7cF1s9Hawz1q?X{z-Y4WANw@d%{)KdV2KbM}-cWTP@>i*r5IFjLVKOKD zVZK6wKl?zPJea|nLGcuRhyVJ-)e^#iOAH0gj0}|J)RdK_*J|o1t-dPc#%} zW~e0uHdY!A_f}UhRy2?`R5dVElh-#=l#|m_)>kxEF)}t%l~XZPHqhTGYlwjoNVc<6 z)|*sT?SEEwYpIhb^1tFi8p$MMQ#XZ!b0Rimd#u^P0=82~xFFfS)IFi3H=LFbl+@=i zZ=r24_@4U5R{uxIw^#;&D-{2&A#9Q1Fh2MoPb^H&7u3*yvNM@~B{$A9@IOec_n-9r zucRj1q^#VIP5u{BZ!44Z+r<|KPQ5Zh|BH4Q{jb`gB>VqQJD?aeX^GjdKJoX&f*-ng z@I9v`YhKiXFikJ-+U2kgI84frJuey9C3&EFaRtWEI0WBs44=Rc10e``Je z2U?dQz2de8vdrJd_*ULi={!{}1d5d?l!R|5tO^R=rzGIRDEzjARew z#{Twx-vNOi4W#7HuNCc`uZlmfj=7fqk%sf_@jaUhcxiZ-vL8|U)n*SH-q?< z$r=2K=Wb(bZER*?L=H1EnSwqx3WmNK;w6x>1l51(vMe=HblfY^Za3f`dS0!*I= z)5st^DM}5Q1=E~f2vVAYY>Cko~XI;6?FCPeTss2`Y?1>sCs zds_oAtqD4!n0$Xrd;ONigJBnt79cs{$=fyr4?qtJf({#1K~q=|5{z(3*?>+cPpmiS zA_6_#=nw$dYBQ-6!2SvSCyAgV zXl0=L8tiV+jZN|_2*S~SajX2s;lC{Q8wXNy%dWu$N7g35=`IcI`osxPPQ3@HI2ZxS zV`(4;^0VJ8=pDhOB>>d}e?|->bB!F289Gtv!3^1^@7lab9fm5O5MW16%-Nfy=-( z;1-Y$WC3|V5l{kD0M)Qd60*YCy*DAw~$6iCu9IJ4*3pQ zrJ$r>qTr<1MJ-%_s#{d|sLH8oshX(Lca|=hE}!lhT@&3X-5UKadQp0H zdTV-b`VjgA`YifN`cL%3^lJ=k4B`x04E78s7|t-`W3&229782u$%zxlGTQI+^B~nVH3yb(vk6@yzkedCaev`Q zaIi?Tn6Y@XM6jf=l(RIkOtaFnin8jly0M0`CbK?b{lxlh7yT}=T?V^6cb(dmvg^sN z_Fao?9BguI)@&%YOKkaUZ`sD!Y1zftjo7`}&$HiUf5kq?LCGP^VZZ_9ILDF2@tR|p zlZI2A(~R>3XDnwS=SR+2E_N9b9 zJ`Fx7UkqOfU)NrWy%KwE_lE7gyZ8Ox#eMwyO!oQjySeYxzHj{8{JQ)n_^T`6`b9_cfaX=!u~t^Kki=>5*Kn1IxAEx)GN#&tSamy zd{y|R@T`b{h^5F$kpht}QF>8TQ9se^qV=N7V&Y;>Vo_ohVq@Yw;%4HZ;sxS82bd4& z90)j&d7$MW%|X?Jh=ZvIKTA+ZC`tH9q)2=^L~%&@5d2W;p+-q+Ni|8dWTs@h6qA&m zRG?IW)POX%w1xCp=?dv-84(#*nM9d-nN3+GS)^>1Y_}Y{++n%1a+Px5L}(m-hBYm8}%Yr-_MGzYbWv^=%$XnoNZ)b`NM(Eg&c zU&m7?Q|GI$u&%dmj_#=5LA?`t_w}arW%aT8W%|no>IUHk&kZRJO$=iUKN_(cIT+nC z>N6HH_BAdro;6W22{U8R(zf1?!_M6<-)_lX z-#)><+u?u%!QrJNtK%`pJjZ1xL#HICFU~T~;m+?|cwOKwk6me99bI!=7mpepy>@i? zn9{Ke$J&mI9}hnM){WN<>Gsr})!ozmkq5PhlSjVCx~H{grstxUnb&QvS*QW@I&{)o z*ZZpXI7|zc1RM3y@=5X;^VRab;ydoAFx1)n=%dLsSAGQt{>iy$JMki{rE zln3ewnjMWm*JAiF!I(z>L;e^1zXYfUTnm`RnqzZt5ZrOxV>~DLG4-BsfDlO-2-FTt z3H%Y{81yKZEf^jAK13qqV#sKyacFiJRTwnv<;ndg&zu|x*9*UM3UbQp)XNCri1QJ{ zr%g`ZJHv1WapuEW*|SM!7tT4It31E&{OR+9ktUG^7g#P}FSJK#M5RYlMZ=>%UR1bv z^WtWVcT7X9Y;1Du`X%V4hB&#n8*#*VpZJdn$_Z(gX)dELw*s4<;UZJZvj5FM0V$^-)o&XzA@T zj1IeD44J z+Y8?pgD>4*cE56d)lz$;_Ty{w*Kg{K>t5CC)z`eydQ<&Y{q2(m)rN|9%J0hGE4?rK zp!A{aqcV67lgg*ZpVdBBHflCLYtm_Y(QMdU*J9T4uGOlwvCW~aqy1R>mk#fau});? zTvuS%TKDN5>YkY1UA@VDd;2o`5B3*-QU3CDz;NLGSG%v>gV4drA>7dV@c9wuk>pW< z(Y!JFvFdT-@y`=SCx$2eC)d7Rm|~l{J$+!hbVhgP{jBrs@EmrI`2EuSp84Db#f6uP zHj8~rsHL^#m>+w7+*?sysb6(k9bF4rr(M6Xad4w@(|ogshytVazfN`~eG6=z>`FsL zMMX_TLrqOXPXqoj($Ub+F)}eQFfuSO?P4Nd+mEfx-|rCc9mmYX%(H73&;RCR*EbMJ zN(guq>aUYs#fY^4xE-2;f)euEmk#NaR0stvKnI?kOFHqD)R659LO#}Y7eE0X=t>DT z2|Ujg00FU2&GXYD~H{9lN7Uiar z{J3RCTUnR;t^R5Kp&mmbE%`h&U$go$`pO^$u`&Jy=CXsU2#b_+0j8a0kQ4?=Cg##i z;Qn&Uyz=V7EBn)0XtI>5?oo-^noVV2l+elZQX2i1e^DdmV#Tw%&1MHHR#k&naX-Q& z>@OkcRpRI#Qe3n>p-kmG$1Px73MjBC2Ha(?w?;W~DP1TROf#ZwHM9A$+lH0-RG0Cd zNMKd_ks+0v;NH9eUy3vlp9@+Ukph{mx-pk6N2ucpE44QzFekFo=Ci&pAFbaoUz;*! z2G2CTgU>D(cH+pRZ%_En8d9k0+w`npKq=p-_IiWAwK~mx=Lvqkc>JBLP(`WH3AMH! zh2ncepqwFZ{G|Qfz@9a;r|@qp(A|fE@%LMH-_{dst{uag7hQcjba^hLN%bQg|726a zIobzVRE|_`m$Z<~!l9?+m+aHaKNvECh+=K5T0n1Iz&qvNb6zS=as;W?OOr~dgcDy3|Yu~`jIHqzxU1>shSkRTj3rZz0 zcFPs6%p)1*g6s#LRUKo6D`iO_>ln^-*mF#`U9_gx+w0eTtG3rZ=y;)%jrNaIPm?#* zjfstS@pDKD#KpE^!n~ges4-X*oAHHGb4OkVPnBjLPp$-BisnWqTtQmn(b-SAP5s@o zLSsC=6UIkqD#k7Axiux#<^W}>xe<r-6ifcm-0m=`;@iU8>JTl>oGQiX7batZ`n<+*BoR!9Fbo_1kgIk z&5ixDCHVM?rph{=_v1I^M!e)oKC$&gK8an5X-~^e+q3xL`hkzq%QMa2vTb`Fb_euD zI4GRJKmC}-hUv|?8LtQ%JVkN;qOU&N+r4xstqVD^j7(2cz7Zs~DqZt;$=r4gKmq>N z&7q-Y2XN+fOX}0yv`_QGrYG|Hf4Ji zR{AN`PC9BCB4acr-S*(45{@_~zpp-czMLW0Ncdxgr1^ z2SoonHBGVVoa6Q_mwZ1YDylax`}(CO#OQMk%gK2T3Qo4RH@=h;t?tPqRp;%?`a<^ir2cmI6FMk z-mY`-@sI~$u+#CJ=|JQmu0x0z;}@Q~6wsrH>J#=zrqS$`*dh1G@jMMGc&F45SHfDOsC5B*lLy9qLN$dSAUGw2-ll|{n z7emzs2RK$#savvR0=UcWhf$a2#mO9UT6)*I!k@Betb9m;|9Z=DkA_GjcueHm12r@z z!aooZ?vEyeW=ED+pMgj51uWP{wtk3QDvPU%nkqInxpx1BQVZpsF4|`cLsGn64C!zk zBhJRcE0V6~GRjjIE>ng4Q)Gg12J&t$CRZ^`DE<_Q5bElrxU88tE zLz$GTi`HFoyZbd2by4w3GLS%*WYy<1lY8Q}H=HyQ0t}jXiZ{u z<*@B#kIXbhj<@So;EB{kfY+XN^ao)`0?FG7Gw%s_EVwrAiHHST87{TkWFl9H2^)%W z^7W)RJ8>lduS4M7^lF!>xD>Qm^Jq$8es8}@&bwxxWB2#)%T`{n4HDFeMwG8_N{l?x zthcE8TBx12xv^St=jB6!Y$}4ywQ*(CyH4ZeBSVSCqvZB`GQFe_Uk?=nT;FV;@4QkB0?J3Ek$7l#H>t?vYRZ zP7VkRP_&iQ{7`$hmsQS756_qdfO6NoQ(&7CiQ_HzF~D%j8k7 z+O6i+@yV)qN^SRJi>wY;?k`?+5?02+m`vkER=gsmyyHwDn;5Q;qT2bEdv*^?%B@RY8sfKlG%SjkgI5Bvzsckhz1^OL&#R&0dU_-I1l z)ic3ZLwWUQr;R5xg5<{}vM7l_!>gK454(qu!*k!d(fdk0+KDaq8kD~XPI0~7N3dEH zSZpdE1Wj|S{=&VKgoy(=95=WjaD#%19GiF78Rq6>x{qthjW5!got9>jP&=?DfjMO+ z2yJ@qc6AKs+J`W;yy|uic2~KA!PTNZZr;A{f^fLy_pm>^}ZR zL%CuVp`G*IX`dUq{^M~=@wyLh#^+x@QsFj^65M6Ra-serrTg{ScIK7(0?iNW61^u5 zBx#vMCR~E}=O#AS#2Ll39zMB#C^r82A|ILBL;v6hL z#vC6a`uOGpk9jr3FvFsj)28^QrT~%%R1tw((Zndq>;mWa7AGx|s$#KBkSVd!i$0cf zNrkTst~A^bzfmHGNN0|P|IjV7R82@zw$=U0g{peC#&wxz;E) zg7PmulxQ@Biug-wpK%ps8;VdMtn(G3g6*S-t$D+z<)HgsK86-&E41(qLwb+S&(;?w z(P;|ZPZ8VDp7tG&VX5)(~>Z?Yt21p%cRZHEu7Hzs11+^u3on~Y11a^?_u8y? zy-Zo_<(1oI$~_vnLDy&Y;+xoR$Fe{0;y|(|j3uG>Eu29+Zl0|SI zXS^b#2M`y`IG5d!v_Z{J`-oMl=Ny_zw>kZt)0=`z=G=B7Zg9Q@74>rnn&p$Nxsv&3 zTD;R)WrPI;wSU~cePp3MM?vvKPc_&&frlbb_ zlH#w*6i=#F-hS_L5v};DUHp8!fJKuhqgU>(>GdC+Qk(fpk4Ht)Yfp|kp=uiRLPG;6 zt)SiD5x2m@#D=c0(u~Fp-TXJBrLwD$YA40+mXzqR0H0G>!y`?O(*u(CySi)N;ijMD zl@3Wm=Xv1s91Afi*3POgrHXO~m=|#weyGcD?A<2lMZ)rjdAftX>+nx@wMLvDB(~*` zG8*tqgL6*!L9O6(H_5*_Sx|# z$}t+_8TV%nn&+6-@M_<+Hhd9%V%-8VF%eXEC zaA-0sd^36Mf4JLRg@51IXu>lwh0>cma}<5H7f-uM8Wm+0q#rD`%8L@QVUDplWtuE& zT*Y^`!n0!SMCH(vv0S^?s^KaE2kJ{r**>Q-_xLZfF0^b|n|hgOK8}Ci9@(mY^y~5W zR{cQ|DGvK@i=>Hlc+XRCd7_MO19`p5kMmuPuZXHV3~%FpEHYMFxXB_m8}RBeUBTlq zik@@XrQ%WH?3dI#V4_a?b*pcBN}KgVWWw#UdMjT(XLn@!e2#E+*ZHH7#kFnK4Jkr8 zEyode2o?${(wb3ub@@ea25kL4ysVlMXgP{(MxY8;%G9FW5}8&K6L}s1foE`p^5mu2 zyoplqOCnR<%{v^EI~cWI^~}OJ#DH4akr}ZM8B+1;*!531BH+3l@s$V+jxSuLcZp*D z!tzqJy?Ukj$9&ZEXO4x>{?CE$dA2HrLea5tJd}&A*5_MKguD@9ef2W)!fX}WoeFlX z>O&e9SSk;GWuSZ6@}Lpx2c{HMeT1)qLskNlL#aP4HOAv zMa_hRww9;6m`h7~z7Luknhowh-jJ)WpBaCO@5&lnLZmi(>E*KXV~N6P@Q7MoT;>DG zw5Yh2Y;8~5GuLiORW(wu(pfQ9r(Y~^`SvCuU$`Zl@_Q;*Kqey(oa$0jaBh&$%f{>< zGEwWT#rz-c0t+MJcF*5*>~@Yhooj3P;wcf}nz8O2K3V0ITM3z@<<_qn=@Go)x68zT zTCaSDt3YiMJapQ(zhZqj@W23H;%*~eNRJ}dq)DvRi{X0yTh0gDyH32&EV>U(YD_{Q zx+BZ7k>?ZAWrRf{WM_Hfou;yv8qW{MS$=qYPn)xN(K*jDwA70Gu8vPP5SzZk6~!=i2~ql$qb&UDrM5}+TmJdD zea*17(ri!4#Ik4sOWC#qg|8wl^Dj8?l`JY9t}j*}H6PcKOIIw>pb$EMTwWLaaV&gT zDXQX$(9@HB#Zm7!oMf#p4@sCXJ{0!)W@A?KT>8+53Pf`{-TQm%HpaOwF2?<@!wPH0 z70rh125C$;^x`M7>Zvdzf_src{QMsdx$NmG2{q4!>6;JbCXc+|J)Jj)s_51~(Jhf6 z1+}Hw48NLZB)k@VV?!eg8V&6#(Td6Vx>|BpE31s_q(rdoMRnn!N3KSJOp|#n?3Qac zQo_3$%FYM$x%hOP^Y#NAYDB-#HWnHkv(Ufd%xP1};DSXy^GnvHm6cKxwRyf_lM8i|6i>_s4HJ za~=>3=(B#{CxL%knR1PHjJCPZo`=!v2~A%E9_s$CQm)jN14MU zR;;aVjseCe@M)%eGUcB#m4==j*2#Vc+0 z*f}nL(;C7LJMd>s&8Ol4b|TQzf0(gmwDDz%e4fbo9i3P2?FrMPD{!rS5sbUd7Ilm! z4zqA0?Rt(;`CC4{eZtpv-L~3go-)_ z^xFL#j~mOAtWQVeKMwDEi#EaJwt9De9~Jl>9CFNuPX@zP7*MRiJ6gBO`C~1#h{HKV zu58U_S$4Q;*mK@)B!s(YL+1^=`orle#-7-;=0XSW#QIww8)$u&rt{>!B-~H4>CLQrw=X{f2h)0 zylyGFJ9khKY1}Fqd?suHgXZE2Or@KEh8>Fm1r_=gSd#?6qQmfU2;)B-K_f@KV~> z6SG~F(+B_$U6iuCct@Mj^-J3BB{n;nSQ>3TORN5iwCKuPxFngCixCP#-ImEjV5&93 zK!DgZ(z#*lWq9GMLQds3v}cw5#=!UX6hw;`f41U1{lYBXRHGX#C&D6_wRpUw?;okP zEy{A!>MCG+!*9R&@|6w2`KaA9ovUXlBbQ=Ao&9|AKL%YyEnnlMcvhzx^1~#J77wS5 z@t2P8)3$*^5An;TO3e(<6#9O9R8v?$`jCuW|K)MF%qcqs4R+(YmGDL?NP>*{)78Zw_#J+1OvDJTR zfVR?j9L~9rq$SW2VwPAH5G5r1B}t)TQee7Z@U`XqKBw#m>{EldM&DY6x3aP&uSC+> zr)kfzMi&zLVTm6$vF*y&GxvUOg56Ysz|3P)mf+zb?JVjjM|E&AO3$&rm~4Y|3A~Fa zqmGM`=x%oo(`FnDy>fqWG1$|6^h-lz*FoAB@fckbmxFF7tE^*Xq%$A_@TY6TT+Zkte-k6nw&h zkfAd}Z!kHP-H#&G9HQajR^P9y@+?Qv_bWaVYPiJ?87Or6K?EdcsNm%)r@CuTqNz6A z4)Qi7NUw4oK?E&1sT`}!Hic4OrZIM+X@5LyQ;2S8oZf$ANQY7Qc-2bEk+XJk?;Urs zWL;K1hHk$xa3r?nf=HTu;{Eeg`;2d8S1n{Ah`=57`yCtZ``UD)M{CvCJ|%P>T@An4 z;yJxIqtv@g+BT}g|81X7=O4F^R6>&L&;S(efVzqMr>HXrcS5* zbz<`{N4<#1jc-sKs+usEp)*x1j%Yn_rC{IN0hKCoVYFASg^RaOg1?GBp1IaNaiesz z?$gNe>cg+SkJ3<0pmKSGGCD@hAGqrCoD{(cv;a7 z7-d$2GSPQbFV1~9KkLpaN@+Z3SAXPvAlqR=q|cL0lcmw3!H;&Vq9W1jVm1Z`HVDOi zj!u#9n)ivWy6e{02C-gs8NL>UsNf59wkHG@;bN5~QG0`qrkt=3#FZVsG^{KC;Fchg zId1AzZ{4*hO||qQBA~#ot!aV_4nnm^)n!|En(#}^yh`&L&SB~^_Tu5HHg$HZzesyM z93Q0J2fdzmVb*tiL(5y$2@VT?!Trr>(}8mM{_0crs~>G|sH)v`VH;hf6|%Rv@jz}f zDd3AX>V(Ep@SE|))%g!*z8(XIE7x@cxR;Lcj`rX^c$z+)bqK;lBYcJqJZNI`(f5Yn znNE7#8=NYe@VmHIP5APz%{IP3|8IE@{lEFYi5r<{e0xWwg{>RYZD(H+8t8Meu_f8i z!=aS7zx>gUf_3f4g+^S{=dy3lxgHN8`WJ}69KSf|tEfyW&)MilA%xXG-7+}(?Alo+ zNxpv1g~qcFlqb7GSt$ueXA7<=CCJ#Cl;kwN;W-d`7Pe;{VK5p$fy`iZz)SiDfiTi@SxA;p`&7v$3$UAY$GKGW3 z7Figk!UpZrcD({oDL5UR2Y#GqG=H9#lkUc#slyWF^e$eCjCrgrqAW72(p1-T&{Y-3e$JvNQj94V zs7`mCRbz8?8MJ$Lc*rc|o1S36!-s99gD)lCjbHw5{FW!NjB46jxI5e3V({KVr)ITt zOR9cs8b!{zOna2o)69%Fi$!_Om5z*~-ut?Z?!=BHexl+h0@3TGUusnh-`)&8IFahC zyxuPPkgehst`ssS?|L&QbjEf)-NGd^LK>SZsUbt-vN2R=<+w0 zw{t+h+|m)2S9YNUu}}YB0D~HQ<^KSXc|6l9-PGBT5%Ama_fUUt7zbA9@C;UxOf->r z4+v!+y?U>*aZR2ad}4fvQVACOffjB&yK644w2&1t6XL$>aIq%c-L(~P&fNp8w9uRW z&(}!Xc@gB}uyR9<2(|snd)v0A#Y&!w z&4G24FayGXToLUx7nRDfdU(f?AORE*ct7mDWz$IdC)27^q+=T`#c5|JCIfeN4qEM*jfu++N&3zb|JsVO~UfwVdTqa)!~c z@b0k#@b|C#YJVn-4``0jpf@YazSMqAtYv{F;g^k%eFpX95?C$Oz7W*3hKtQvbUp$# zE!o3f+ji2bhK-ejUv;ht;5)@k4H=QuiVGFum((A;r-hoHVGucUCCdU*KZ$vY)9b0$ zjU)`}!GSBPk)`(o;WSBbP$pCoUA5e|9^PWFnwpXH$5M+PbrKLvnlBJ<-^RAaSLJ)g z>k7q2;sMgbU#9go1&K?O+hNH^GNiM~-A~ZL5Zu@*9 zh^)oTWMaRQBOIIV?9+@(>=+pU9>#BqEUzooiyHiFkFB-`cM8GmQAcMez zN)naVeLEB`Wk|t*=)twO(|UNR(zh-YDK^DpZ~J7OH2H7Kx}wICvbaY2a84<~fgDGD zN$KvN(&lr$2Z3+68~i=gu=H6l<5ReT@tpxW5%kl*!(vLZDu#EA*cDa)4q)j)EPWf( zSu!$|1%qGW0FV!Ew5+$`QsbohW76>B6_#knm~C-kTl&2pn#bGk{6|sgYiv~j3kv~W zocMXyG2LraV1NJC;z(3If$$r&e9AXHIjeS8k*`$rGP1zzJe4MI)^~A;ik)iX80>GQDocrqjiZn8D^YEsSFgdfbs8W1Hy8u;Jon@X? zUOfvt24Bn18%E<*aj0NA{eqGV0fM))lw3-m+AaBbGZTR7UAqYl)kZFYQ{H1#8=(LxH6u6wWWB6_o!)QHe`DAln#C--=nku)A4MPLKhbgN*pVPr zRR9eE0=MUuU39TxEr7A%TS&{QwP0=X7eWq|yCWs38&Y`C8V6CwjRy0ZgK7a;Z@b@B z2o^ovwM5z?K=%)_f)qOdZP`&_V_%!`Uf1{15C7NU)5<)>kP@yePTg($YsimO+${3# zXl>?cxdxi{i}vDSvfR7sVzE%k!Dt z54V+VI@lt^(fU?TNkq=Zg{R6Q`^ep}9c{Gm_f+-Op=9a(Yt$Keuo<9Z7T@MJ(xY!^ z@2vcci+mYa!^#2~n6JZeVm-BZl%^b!83_vMyZ-=C_)oHo17>7J759*=1;Jh)MHmrJ zH<(MKh97#@A5|doC>rh;9KoPAN$0)SYXEydKSeAv&}6XyvXS06QzXYun^CTvl~Ah7 z)7a>5$FjEOGqpX3cXd(3o)xz-ovT(eZHy=;!rB_u1|v5B_Y+8K!~$(V)`uGA^wX>u z02C@x?zV2-Xgvki{{TUzP0}-C-D*~mw8jj+;6A#}(qr<2b^G|#=|BI_;LFapsP{D!lK7jE6_8V(&_R_qK8f>Xtfhy`W40RsnAFhlzT5YkA`jB|@ z1K&jQ8kqx)tEl8TTeymN3`ECp+zpB4uD$gZ959YH9poLScI`MCn-#5ESj!dMx3SOz zb>=ELG3chtTsg)#EXsp{B?I!>QwEYEX_0N*lxM>tePs(E27!+!ot!2N(B%Pn`{B-5~iyp*_7BbBQjTST) z{NGTv*&F`=mjFM~-Tr@>zU5xCu_@EKt44^=vJ6KT!@*Igj`s5(_nY9qj#lw}H63yy zS0S+{w&+v^;^g>=9rd0nP%@;zOnDJNQzfpV=HzhWPnde&rS+DN!#FXqxDR`;4GmR< zu;TTUDx^(&Etx?Y_xnXo5>Fec0zqk-%kgRew|zVY8`KcQTgd+a$ZZdnNw>JtwnLJ< zol0!2Rmw2`0Jh*dx*NAZ8} zP!D1>#DWNaWz_u!i3Y6wF)S6YW7unMc}G=b#BpKI?X6^GY03KPbl{{Xn=!W7`409! zDDSN&gnW;K>ujgW5VOYY!+C-|NMGLJPkfz=e^2XpSvF8iUJH*i!>?sqI+HJss2{hN8 zswoTR5Ada-|IzvUvTnYiTTV8y?F84Gk4d|;r5!ot%-q{v5ZB_ zvf@Rn*j7sn77mfbv{8BfGs?!CD{N1s^ zxgiAEO^GJfF63y;2L2jsQ7BUROTi+R%^@zQZ?CB8-c{Ozh!6CG@3?){{S^^ID=4Z zWMSNmny+VlD^(tg1fLRd0F8U;eUB1&k-TIFQbO?uUO%)_`%#d_{{ScYV6u^QCygId z)?@23#^JWvufvh|8hDW(sm(iX3ZY4~k+8V#J*)gQS}`MuJi%b=ec~xO*QwA9Uzb<^ORXI(oP6HbNPXSwV8PvQvQNHGUSBtO z6|dXFKd+VN(|_E29JOe(UGBCbc0z=1*372gRX%!^9dTmeht#@$YVO6zNxPJbk9AW# z1E{iqM?2NXNRZ=yzLhr3k^nOI(h?LHlZek;y8l1P4aA0d&`p1Bs<(n-XqV!l~X^ z^IL1#xOa1>3Xdi>SLI{!)60ppj>p8zPNvL$6HD?50JnEcM)G5-K9R;La0xWGWdQU&&*AaLTxPUWfV=Xju#og@x0IH4Bz@8=BCdMYiR<($`3*L!9xaY~7>R7W}0(1&AH>YZGYQ!=uiu)(*Du0C(uRT7iLl~t}s5o7uqsY;}x$SMZi zEyAl)fc1W%l)XL1IBmyCHt4}mI~uRCVxrkuLMM(t2F+aDDmLyb_+0b(YA?e`f2MUV znz$_|P-7HPw%;xjZSD7({{RiCb7Y=H)bCE~xY1*XanQ#SsCDy$769r@hnd%vRQ6}V zeG)PC#Yt7-k{ynte4B!zpyy*|X1@ie_b;kfr1dTGlb1SAk0IRzikDk(YlGq%d{n;w z0G&I+V0CFaK2&+wkvxZR5_xO_fPNFh;iuPmzpZ)g(dcr_SIAtmgRK)HKn1n2{8LAC>!$gUw^vZ`jG$A`Lbtn>>E?a z*Do>OM}|nGj5{D#Tad@^4m$VLS!Eh;24dF%LXAOQRy;uLsO~CPoo_C}ZP;4tuWiQ~ zM$2vT!;%4AJky1ga-tljW^u7#5Rb+7+ z_@gBmf!%9rZ^+()5+pJsW$PG;d@{%wc>WdIx4rIe7OH%eCmi6J0njCtNEa)Ah_UA* z_k2GMS5x4*zDYP18%A-k?Gci5} zB->H0yv;kAQRxQ z7h~P~#+3_1Yh)Y<<9bw#EI`{$O{`5L88Nq99W~eWngE9w9c`yR`%nx=6pLb4!+)yO z>?me|+APA$03^;d+G_kmx0i}mwqed`t8(lrBX=uhIWu69# zP#s9n9w7Hxu2QO(P3gJaM=pFwWd2kei_It^J@@db=dX1>hbt#b=&|BJqibZ#7$!#8 z0A?HxylwLS8qZXh$IqEs(vrkou7hx}9}5w{e6)Sf>S8468E_bm0)AbPi4ryOqj3ct zb<>qUw566D=%18^2giyzPmo9hAO=%@BI3t|IiCe}Efz0Vks0tMNn-?gaF*EWr;3tv zHt+bBoR)kR&&uiO{4$8`LfCe;B-D6d$@7uT(0IK0JCmy z2^Q?s@dk<_Ljgd(^ko2PBHvNpZ7V*G4>gr9q>=8qaMmDq4pk~Iku*#^9vl3p?cu_l z2fWnu=Z1+Ol{s&jk|ndtcpMv?H#|nORtd);gkW>U|SoYI{5hoUKhF4 zwJHwHy|C0r6EKDLqd8!vKnU=u)z0EgGSp;|#}sBZTkR!_HohUp!Zg$F)6*d=GAgvi zm=ojJ2gTlRSIwUZu_@6Xb1Zgd5-TP4js#lf#@Z3vQ{3_;@X;79=$%Lfwz0T2*Ow1< zMO+lH4hbG-k_eG52ZW1h-`h$RY7$Qn19JiZ=63M#+m$*-;q&9L3_Q>{k}uu`xKg2Q z=yUnNMo)}d;E-?r6ctUjC}0X|dwJA{yqO{bz*v3nwv7#g_-sz3*iZ%;0d5Zw#CB_H zpdw2dUkTJ(Q{POQ%OYvor$a`Ey!Rk@uig78>0pt)+h`=Q;oEvN8j}Ygs_$i8#@pD@ zqXcXTG2$&NEQ{EuUu{3O2^ohOVlaerh+Lm3ZMjYLV%FzVdi<(Hk)I|5vQ3gSekHNm zb|1Tc4z&K%v9u*~3*^KK%@DX33%mjG6&h~;0CiT3HPIWV$nr{5#9Rg?2>^RR(^{Xj z(PcjdNb_XH$i`;?0T4~0*-oXw2ZgxPozZzY&?Fd;x=Xe^eXNpeZB6Zd{{VeY7Vq?Z zNSIuwSumauGEz1PGB=CDHCHF*UzNWLR+jiiI_@||f;S1Xa)YE=-%JO_dLIiEyf4y1t z{h6%hnPLTjTk})8-%k-y6%o%Xxh6&`tI0*1+1*uvm|{>vviL#V9FT3;JIxk@pZOBV z0w`8s0w0);XK2&->Su^)d`t#5^Fa=vd2xkPe`p>Rr3I~etH!wKv9;uFHx{+P7Pz+r z9i!r=NSA`e8Ycdbu-jml?Z0KuI(w#4NOG2Na(0;5knB#k=WA=-S2OYQky|EAUZE6` z?N!svLb?lEjr+gL^3$GTZ;F0*mE&2}N^T@=OEBGSbvN*zinFuJO)S$Fjhgo*NNt31 z9lCR(LbP~gi9)i4-~oGILB_)W0P3QHaXd_XE3m>mjwDk}iEk}f0mL5L8dx5i1|S2W4#bOEo&r+z{I7~MFMc*0X#ZXpSWsKypwhCQQ?c&e0)!352Q`Ef~jB0Hv8IIk}aO!95A z@!`rw81fgwVxdnT=Gx=~*tJ?%XQRi3g@PoWa=v1;Y+s0R@Ysr(PYo>CDy=3wm&bjg zWdiI)u1fcEq6Q_;P|-J<0G2x;;Oa>`KqJL{qL@ubg%E-hk>X~&?)JKs8f+thE&iHv z(oOVCVU7IK#WAP;oFw=`Cq)hq*r8dQ7MP04^SWf*7>2W z;XE#E#GBY(vbp1~yrN53i#^D{vWte4z}!`VyI$Ab;{9Jxth)J8`IQh_M;q;+UkZ!% zsJ^u|Q6DOj#!HZZXaE2ZIqO8AXtAYu!&_i&Y@^MgqRj zq>u-O=iy(&M5-a3fM$X~ArY)A;cM?;M`^GXeB7zNNn;#|FcoBja;Q6<&ZUyzp59`# z>&Hmv;KxbndHHxtjT}zoJ(TJ+;;qWtXzKYB;bO(~FH*>Xzcj)ug*OCtj(oMO_9z4B zKD!s2BC|mfF!IZ@|w zLc&sPqWlS?L5`7;14v$N7-U&G zSS~W@;TU9P-wtT{6&bn!O7gCFFnG+)Y>t4h4(%295%nYf|l8L?byD&E$ z^`BoViMI6bP$5W&vnaOi3Qz8eil|CoPxW8S10)-n&G%R}zQv~?)T;V#F*XgmgKBpY zN&P#i9H%7o#G)WfDo-WkKCMkG8kFgsA0ilR`ZH;#hfe+^(WOs6NmCVq`0Zk87%R}R zP%oY?oYa6p_fj!G;<{9?<&1DtSlsLO(e^AAx<^Hfl3_p^+xTm0KGb2eJ{B};abZwC z@Dy4wtcF~PEgRj~-GxcLkJOhNC8EgFX8}*hKMz%e!6d}o{Gd^Qt4TIgci+c{XrhAg zV+EUJ(w8HE<7$x1GWABx3lUPQ+s3FG%9}Dd0g@ok_)P)fHV0FL!~r&4rkHwH6} z>28?CnOn?=8-~BtMvOe&KMxK)`#_KG+fz&Az!`FwUN0%0CN>3mQ-x6Q^og^&v#8wO zSp>%10jFriucu<+ra)}hQ!FY#w*hWKmYA{(7s_eoJ04;dC|ik3akVlZ{xcF@my%sC z6U4z#ec`AgoS_5Jv7Ri1k(kAiDDul&!h6jio55+40UXYfq)9?8Vd;b9Gui0Ik{5bE)ie&Wx>I47T8ucyd literal 0 HcmV?d00001 From d68c5ac8eb5ed5aaf21dca245ca77548c29d71cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 22:52:59 +0000 Subject: [PATCH 032/701] Bump joda-time:joda-time from 2.13.1 to 2.14.0 Bumps [joda-time:joda-time](https://github.com/JodaOrg/joda-time) from 2.13.1 to 2.14.0. - [Release notes](https://github.com/JodaOrg/joda-time/releases) - [Changelog](https://github.com/JodaOrg/joda-time/blob/main/RELEASE-NOTES.txt) - [Commits](https://github.com/JodaOrg/joda-time/compare/v2.13.1...v2.14.0) --- updated-dependencies: - dependency-name: joda-time:joda-time dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 886aa3fb36cb..6fdf08157363 100644 --- a/pom.xml +++ b/pom.xml @@ -1542,7 +1542,7 @@ joda-time joda-time - 2.13.1 + 2.14.0 com.sun.mail From 3fa57d7c0e4da46e745afd995578c5ae9922565b Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 7 Oct 2024 11:42:35 +0200 Subject: [PATCH 033/701] More robust ORCID accessToken init, REST con. usage * Ensure that http client / IO exceptions don't cause a total DSpace startup failure because of unhandled exceptions in Spring service init methods. * Centralise access token retrieval method in factory utils. * Check for NULL rest connector since that can now happen and handle gracefully, with error logging (cherry picked from commit b72344ecfbdd85ce1cb98bfe0ee0d76ebd9439e6) --- .../orcid/Orcidv3SolrAuthorityImpl.java | 93 +++++++++---------- .../impl/OrcidV3AuthorDataProvider.java | 83 +++++++++-------- .../model/factory/OrcidFactoryUtils.java | 55 +++++++++++ .../org/dspace/authority/orcid/MockOrcid.java | 17 +++- 4 files changed, 157 insertions(+), 91 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java index 6753a5d113b7..494daa97734a 100644 --- a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java +++ b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java @@ -7,27 +7,22 @@ */ package org.dspace.authority.orcid; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.URLEncoder; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.authority.AuthorityValue; import org.dspace.authority.SolrAuthorityInterface; import org.dspace.external.OrcidRestConnector; import org.dspace.external.provider.orcid.xml.XMLtoBio; -import org.json.JSONObject; +import org.dspace.orcid.model.factory.OrcidFactoryUtils; import org.orcid.jaxb.model.v3.release.common.OrcidIdentifier; import org.orcid.jaxb.model.v3.release.record.Person; import org.orcid.jaxb.model.v3.release.search.Result; @@ -50,6 +45,11 @@ public class Orcidv3SolrAuthorityImpl implements SolrAuthorityInterface { private String accessToken; + /** + * Maximum retries to allow for the access token retrieval + */ + private int maxClientRetries = 3; + public void setOAUTHUrl(String oAUTHUrl) { OAUTHUrl = oAUTHUrl; } @@ -62,46 +62,32 @@ public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; } + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + /** * Initialize the accessToken that is required for all subsequent calls to ORCID */ public void init() { - if (StringUtils.isBlank(accessToken) - && StringUtils.isNotBlank(clientSecret) - && StringUtils.isNotBlank(clientId) - && StringUtils.isNotBlank(OAUTHUrl)) { - String authenticationParameters = "?client_id=" + clientId + - "&client_secret=" + clientSecret + - "&scope=/read-public&grant_type=client_credentials"; - try { - HttpPost httpPost = new HttpPost(OAUTHUrl + authenticationParameters); - httpPost.addHeader("Accept", "application/json"); - httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); - - HttpClient httpClient = HttpClientBuilder.create().build(); - HttpResponse getResponse = httpClient.execute(httpPost); - - JSONObject responseObject = null; - try (InputStream is = getResponse.getEntity().getContent(); - BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { - String inputStr; - while ((inputStr = streamReader.readLine()) != null && responseObject == null) { - if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) { - try { - responseObject = new JSONObject(inputStr); - } catch (Exception e) { - //Not as valid as I'd hoped, move along - responseObject = null; - } - } - } - } - if (responseObject != null && responseObject.has("access_token")) { - accessToken = (String) responseObject.get("access_token"); - } - } catch (Exception e) { - throw new RuntimeException("Error during initialization of the Orcid connector", e); - } + // Initialize access token at spring instantiation. If it fails, the access token will be null rather + // than causing a fatal Spring startup error + initializeAccessToken(); + } + + public void initializeAccessToken() { + // If we have reaches max retries or the access token is already set, return immediately + if (maxClientRetries <= 0 || org.apache.commons.lang3.StringUtils.isNotBlank(accessToken)) { + return; + } + try { + accessToken = OrcidFactoryUtils.retrieveAccessToken(clientId, clientSecret, OAUTHUrl).orElse(null); + } catch (IOException e) { + log.error("Error retrieving ORCID access token, {} retries left", --maxClientRetries); } } @@ -116,7 +102,7 @@ public void setOrcidRestConnector(OrcidRestConnector orcidRestConnector) { */ @Override public List queryAuthorities(String text, int max) { - init(); + initializeAccessToken(); List bios = queryBio(text, max); List result = new ArrayList<>(); for (Person person : bios) { @@ -135,7 +121,7 @@ public List queryAuthorities(String text, int max) { */ @Override public AuthorityValue queryAuthorityID(String id) { - init(); + initializeAccessToken(); Person person = getBio(id); AuthorityValue valueFromPerson = Orcidv3AuthorityValue.create(person); return valueFromPerson; @@ -151,11 +137,14 @@ public Person getBio(String id) { if (!isValid(id)) { return null; } - init(); + if (orcidRestConnector == null) { + log.error("ORCID REST connector is null, returning null Person"); + return null; + } + initializeAccessToken(); InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); XMLtoBio converter = new XMLtoBio(); - Person person = converter.convertSinglePerson(bioDocument); - return person; + return converter.convertSinglePerson(bioDocument); } @@ -167,10 +156,16 @@ public Person getBio(String id) { * @return List */ public List queryBio(String text, int start, int rows) { - init(); if (rows > 100) { throw new IllegalArgumentException("The maximum number of results to retrieve cannot exceed 100."); } + // Check REST connector is initialized + if (orcidRestConnector == null) { + log.error("ORCID REST connector is not initialized, returning empty list"); + return Collections.emptyList(); + } + // Check / init access token + initializeAccessToken(); String searchPath = "search?q=" + URLEncoder.encode(text) + "&start=" + start + "&rows=" + rows; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index 125da8f7c67b..c7e41171a5bb 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -7,24 +7,17 @@ */ package org.dspace.external.provider.impl; -import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.dto.MetadataValueDTO; @@ -32,7 +25,7 @@ import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.AbstractExternalDataProvider; import org.dspace.external.provider.orcid.xml.XMLtoBio; -import org.json.JSONObject; +import org.dspace.orcid.model.factory.OrcidFactoryUtils; import org.orcid.jaxb.model.v3.release.common.OrcidIdentifier; import org.orcid.jaxb.model.v3.release.record.Person; import org.orcid.jaxb.model.v3.release.search.Result; @@ -60,6 +53,11 @@ public class OrcidV3AuthorDataProvider extends AbstractExternalDataProvider { private XMLtoBio converter; + /** + * Maximum retries to allow for the access token retrieval + */ + private int maxClientRetries = 3; + public static final String ORCID_ID_SYNTAX = "\\d{4}-\\d{4}-\\d{4}-(\\d{3}X|\\d{4})"; private static final int MAX_INDEX = 10000; @@ -78,47 +76,37 @@ public OrcidV3AuthorDataProvider() { * @throws java.io.IOException passed through from HTTPclient. */ public void init() throws IOException { - if (StringUtils.isNotBlank(clientSecret) && StringUtils.isNotBlank(clientId) - && StringUtils.isNotBlank(OAUTHUrl)) { - String authenticationParameters = "?client_id=" + clientId + - "&client_secret=" + clientSecret + - "&scope=/read-public&grant_type=client_credentials"; - HttpPost httpPost = new HttpPost(OAUTHUrl + authenticationParameters); - httpPost.addHeader("Accept", "application/json"); - httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); - - HttpClient httpClient = HttpClientBuilder.create().build(); - HttpResponse getResponse = httpClient.execute(httpPost); - - JSONObject responseObject = null; - try (InputStream is = getResponse.getEntity().getContent(); - BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { - String inputStr; - while ((inputStr = streamReader.readLine()) != null && responseObject == null) { - if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) { - try { - responseObject = new JSONObject(inputStr); - } catch (Exception e) { - //Not as valid as I'd hoped, move along - responseObject = null; - } - } - } - } - if (responseObject != null && responseObject.has("access_token")) { - accessToken = (String) responseObject.get("access_token"); - } + // Initialize access token at spring instantiation. If it fails, the access token will be null rather + // than causing a fatal Spring startup error + initializeAccessToken(); + } + + /** + * Initialize access token, logging an error and decrementing remaining retries if an IOException is thrown. + * If the optional access token result is empty, set to null instead. + */ + public void initializeAccessToken() { + // If we have reaches max retries or the access token is already set, return immediately + if (maxClientRetries <= 0 || StringUtils.isNotBlank(accessToken)) { + return; + } + try { + accessToken = OrcidFactoryUtils.retrieveAccessToken(clientId, clientSecret, OAUTHUrl).orElse(null); + } catch (IOException e) { + log.error("Error retrieving ORCID access token, {} retries left", --maxClientRetries); } } @Override public Optional getExternalDataObject(String id) { + initializeAccessToken(); Person person = getBio(id); ExternalDataObject externalDataObject = convertToExternalDataObject(person); return Optional.of(externalDataObject); } protected ExternalDataObject convertToExternalDataObject(Person person) { + initializeAccessToken(); ExternalDataObject externalDataObject = new ExternalDataObject(sourceIdentifier); if (person.getName() != null) { String lastName = ""; @@ -167,6 +155,11 @@ public Person getBio(String id) { if (!isValid(id)) { return null; } + if (orcidRestConnector == null) { + log.error("ORCID REST connector is null, returning null ORCID Person Bio"); + return null; + } + initializeAccessToken(); InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); Person person = converter.convertSinglePerson(bioDocument); try { @@ -188,12 +181,18 @@ private boolean isValid(String text) { @Override public List searchExternalDataObjects(String query, int start, int limit) { + initializeAccessToken(); if (limit > 100) { throw new IllegalArgumentException("The maximum number of results to retrieve cannot exceed 100."); } if (start > MAX_INDEX) { throw new IllegalArgumentException("The starting number of results to retrieve cannot exceed 10000."); } + // Check REST connector is initialized + if (orcidRestConnector == null) { + log.error("ORCID REST connector is not initialized, returning empty list"); + return Collections.emptyList(); + } String searchPath = "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&start=" + start @@ -218,9 +217,6 @@ public List searchExternalDataObjects(String query, int star } catch (IOException e) { log.error(e.getMessage(), e); } - if (Objects.isNull(bios)) { - return Collections.emptyList(); - } return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); } @@ -231,6 +227,11 @@ public boolean supports(String source) { @Override public int getNumberOfResults(String query) { + if (orcidRestConnector == null) { + log.error("ORCID REST connector is null, returning 0"); + return 0; + } + initializeAccessToken(); String searchPath = "search?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&start=" + 0 + "&rows=" + 0; diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java index 4b8c1178efeb..38aa611ff3a0 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java @@ -7,10 +7,21 @@ */ package org.dspace.orcid.model.factory; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.json.JSONObject; /** * Utility class for Orcid factory classes. This is used to parse the @@ -65,4 +76,48 @@ private static String[] parseConfiguration(String configuration) { return configurations; } + /** + * Retrieve access token from ORCID, given a client ID, client secret and OAuth URL + * + * @param clientId ORCID client ID + * @param clientSecret ORCID client secret + * @param oauthUrl ORCID oauth redirect URL + * @return response object as Optional string + * @throws IOException if any errors are encountered making the connection or reading a response + */ + public static Optional retrieveAccessToken(String clientId, String clientSecret, String oauthUrl) + throws IOException { + if (StringUtils.isNotBlank(clientSecret) && StringUtils.isNotBlank(clientId) + && StringUtils.isNotBlank(oauthUrl)) { + String authenticationParameters = "?client_id=" + clientId + + "&client_secret=" + clientSecret + + "&scope=/read-public&grant_type=client_credentials"; + HttpPost httpPost = new HttpPost(oauthUrl + authenticationParameters); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); + + HttpResponse response; + try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + response = httpClient.execute(httpPost); + } + JSONObject responseObject = null; + if (response != null && response.getStatusLine().getStatusCode() == 200) { + try (InputStream is = response.getEntity().getContent(); + BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, + StandardCharsets.UTF_8))) { + String inputStr; + while ((inputStr = streamReader.readLine()) != null && responseObject == null) { + if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) { + responseObject = new JSONObject(inputStr); + } + } + } + } + if (responseObject != null && responseObject.has("access_token")) { + return Optional.of((String) responseObject.get("access_token")); + } + } + // Return empty by default + return Optional.empty(); + } } diff --git a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java index 562aa86a585e..82dc3fa5cc12 100644 --- a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java +++ b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java @@ -26,8 +26,24 @@ */ public class MockOrcid extends Orcidv3SolrAuthorityImpl { + public MockOrcid() { + setupMockConnector(); + setAccessToken("mock-access-token"); + } + @Override public void init() { + // Empty implementation as setup is now done in constructor + } + + @Override + public void initializeAccessToken() { + if (getAccessToken() == null) { + setAccessToken("mock-access-token"); + } + } + + private void setupMockConnector() { OrcidRestConnector orcidRestConnector = Mockito.mock(OrcidRestConnector.class); when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @@ -53,5 +69,4 @@ public InputStream answer(InvocationOnMock invocation) { setOrcidRestConnector(orcidRestConnector); } - } From 1fe697a19ec24b7223346e00c658df1cfec0788b Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sun, 30 Mar 2025 22:30:25 +0200 Subject: [PATCH 034/701] Fix some ORCID mock / test usage (cherry picked from commit 038ddeee9795c75b876ce1ea73db2b908a8c939c) --- .../spring/api/orcid-authority-services.xml | 2 +- .../org/dspace/authority/orcid/MockOrcid.java | 57 ++++++++++++------- .../app/rest/VocabularyRestRepositoryIT.java | 14 +++++ 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/orcid-authority-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/orcid-authority-services.xml index 4a73b215cd4b..3e38055b678a 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/orcid-authority-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/orcid-authority-services.xml @@ -16,7 +16,7 @@ - + diff --git a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java index 82dc3fa5cc12..511df79f1e50 100644 --- a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java +++ b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java @@ -26,41 +26,47 @@ */ public class MockOrcid extends Orcidv3SolrAuthorityImpl { - public MockOrcid() { - setupMockConnector(); - setAccessToken("mock-access-token"); - } + OrcidRestConnector orcidRestConnector; @Override public void init() { - // Empty implementation as setup is now done in constructor + initializeAccessToken(); + orcidRestConnector = Mockito.mock(OrcidRestConnector.class); } - @Override - public void initializeAccessToken() { - if (getAccessToken() == null) { - setAccessToken("mock-access-token"); - } - } - - private void setupMockConnector() { - OrcidRestConnector orcidRestConnector = Mockito.mock(OrcidRestConnector.class); + /** + * Call this to set up mocking for any test classes that need it. We don't set it in init() + * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing + */ + public void setupNoResultsSearch() { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?"), ArgumentMatchers.any())) - .thenAnswer(new Answer() { - @Override - public InputStream answer(InvocationOnMock invocation) { - return this.getClass().getResourceAsStream("orcid-search-noresults.xml"); - } - }); + .thenAnswer(new Answer() { + @Override + public InputStream answer(InvocationOnMock invocation) { + return this.getClass().getResourceAsStream("orcid-search-noresults.xml"); + } + }); + } + /** + * Call this to set up mocking for any test classes that need it. We don't set it in init() + * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing + */ + public void setupSingleSearch() { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?q=Bollini"), ArgumentMatchers.any())) - .thenAnswer(new Answer() { + .thenAnswer(new Answer() { @Override public InputStream answer(InvocationOnMock invocation) { return this.getClass().getResourceAsStream("orcid-search.xml"); } }); + } + /** + * Call this to set up mocking for any test classes that need it. We don't set it in init() + * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing + */ + public void setupSearchWithResults() { when(orcidRestConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) - .thenAnswer(new Answer() { + .thenAnswer(new Answer() { @Override public InputStream answer(InvocationOnMock invocation) { return this.getClass().getResourceAsStream("orcid-person-record.xml"); @@ -69,4 +75,11 @@ public InputStream answer(InvocationOnMock invocation) { setOrcidRestConnector(orcidRestConnector); } + + @Override + public void initializeAccessToken() { + if (getAccessToken() == null) { + setAccessToken("mock-access-token"); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java index 30890d7ef838..7a3bc738eb66 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java @@ -22,6 +22,7 @@ import org.dspace.authority.AuthorityValueServiceImpl; import org.dspace.authority.PersonAuthorityValue; import org.dspace.authority.factory.AuthorityServiceFactory; +import org.dspace.authority.orcid.MockOrcid; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.content.Collection; @@ -29,11 +30,13 @@ import org.dspace.content.authority.service.ChoiceAuthorityService; import org.dspace.core.service.PluginService; import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; /** * This class handles all Authority related IT. It alters some config to run the tests, but it gets cleared again @@ -56,6 +59,17 @@ public class VocabularyRestRepositoryIT extends AbstractControllerIntegrationTes @Before public void setup() throws Exception { super.setUp(); + + // Explicitly set stubbing for the MockOrcid class. We don't do it in the init() or constructor + // of the MockOrcid class itself or Mockito will complain of unnecessary stubbing in certain other + // AbstractIntegrationTest implementations (depending on how config is (re)loaded) + ApplicationContext applicationContext = DSpaceServicesFactory.getInstance() + .getServiceManager().getApplicationContext(); + MockOrcid mockOrcid = applicationContext.getBean(MockOrcid.class); + mockOrcid.setupNoResultsSearch(); + mockOrcid.setupSingleSearch(); + mockOrcid.setupSearchWithResults(); + configurationService.setProperty("plugin.named.org.dspace.content.authority.ChoiceAuthority", new String[] { "org.dspace.content.authority.SolrAuthority = SolrAuthorAuthority", From 4c73e4b01f5a11b48e6e9e4f2e07bb6b1ef0670e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 7 Apr 2025 10:54:33 -0500 Subject: [PATCH 035/701] Update reusable-docker-build to use Ubuntu ARM64 runner for those images --- .github/workflows/reusable-docker-build.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 3b74f250b539..aec03603cef0 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -87,17 +87,16 @@ jobs: matrix: # Architectures / Platforms for which we will build Docker images arch: [ 'linux/amd64', 'linux/arm64' ] - os: [ ubuntu-latest ] isPr: - ${{ github.event_name == 'pull_request' }} # If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work. # The below exclude therefore ensures we do NOT build ARM64 for PRs. exclude: - isPr: true - os: ubuntu-latest arch: linux/arm64 - runs-on: ${{ matrix.os }} + # If ARM64, then use the Ubuntu ARM64 runner. Otherwise, use the Ubuntu AMD64 runner + runs-on: ${{ matrix.arch == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} steps: # This step converts the slashes in the "arch" matrix values above into dashes & saves to env.ARCH_NAME @@ -123,10 +122,6 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU emulation to build for multiple architectures - uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-buildx-action - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 From bfe7e7871e443aefe783976672c5e36ef349dd9b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 7 Apr 2025 14:57:31 -0500 Subject: [PATCH 036/701] Remove unused SWORD v1 client code. This is "dead code" which is unmaintained and obsolete --- .../java/org/purl/sword/client/Client.java | 463 ---------- .../purl/sword/client/ClientConstants.java | 40 - .../org/purl/sword/client/ClientFactory.java | 116 --- .../org/purl/sword/client/ClientOptions.java | 605 ------------- .../org/purl/sword/client/ClientType.java | 23 - .../java/org/purl/sword/client/CmdClient.java | 445 --------- .../purl/sword/client/DebugOutputStream.java | 44 - .../purl/sword/client/MessageOutputPanel.java | 138 --- .../purl/sword/client/PostDestination.java | 140 --- .../org/purl/sword/client/PostDialog.java | 599 ------------- .../org/purl/sword/client/PostMessage.java | 344 ------- .../purl/sword/client/PropertiesDialog.java | 240 ----- .../org/purl/sword/client/SWORDClient.java | 85 -- .../sword/client/SWORDClientException.java | 42 - .../org/purl/sword/client/SWORDComboBox.java | 129 --- .../org/purl/sword/client/SWORDFormPanel.java | 144 --- .../org/purl/sword/client/ServiceDialog.java | 227 ----- .../org/purl/sword/client/ServicePanel.java | 843 ------------------ .../sword/client/ServiceSelectedListener.java | 23 - .../org/purl/sword/client/ServletClient.java | 455 ---------- .../java/org/purl/sword/client/Status.java | 61 -- 21 files changed, 5206 deletions(-) delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/Client.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/ClientConstants.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/ClientFactory.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/ClientOptions.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/ClientType.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/CmdClient.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/DebugOutputStream.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/MessageOutputPanel.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/PostDestination.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/PostDialog.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/PostMessage.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/PropertiesDialog.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/SWORDClient.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/SWORDClientException.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/SWORDComboBox.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/SWORDFormPanel.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/ServiceDialog.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/ServicePanel.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/ServiceSelectedListener.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/ServletClient.java delete mode 100644 dspace-sword/src/main/java/org/purl/sword/client/Status.java diff --git a/dspace-sword/src/main/java/org/purl/sword/client/Client.java b/dspace-sword/src/main/java/org/purl/sword/client/Client.java deleted file mode 100644 index c6d335d8dd16..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/Client.java +++ /dev/null @@ -1,463 +0,0 @@ -/** - * 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.purl.sword.client; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.NoSuchAlgorithmException; -import java.util.Properties; - -import org.apache.http.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.StatusLine; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.conn.params.ConnRoutePNames; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.FileEntity; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.params.HttpParams; -import org.apache.logging.log4j.Logger; -import org.purl.sword.base.ChecksumUtils; -import org.purl.sword.base.DepositResponse; -import org.purl.sword.base.HttpHeaders; -import org.purl.sword.base.ServiceDocument; -import org.purl.sword.base.SwordValidationInfo; -import org.purl.sword.base.UnmarshallException; - -/** - * This is an example Client implementation to demonstrate how to connect to a - * SWORD server. The client supports BASIC HTTP Authentication. This can be - * initialised by setting a username and password. - * - * @author Neil Taylor - */ -public class Client implements SWORDClient { - /** - * The status field for the response code from the recent network access. - */ - private Status status; - - /** - * The name of the server to contact. - */ - private String server; - - /** - * The port number for the server. - */ - private int port; - - /** - * Specifies if the network access should use HTTP authentication. - */ - private boolean doAuthentication; - - /** - * The username to use for Basic Authentication. - */ - private String username; - - /** - * User password that is to be used. - */ - private String password; - - /** - * The userAgent to identify this application. - */ - private String userAgent; - - /** - * The client that is used to send data to the specified server. - */ - private final DefaultHttpClient client; - - /** - * The default connection timeout. This can be modified by using the - * setSocketTimeout method. - */ - public static final int DEFAULT_TIMEOUT = 20000; - - /** - * Logger. - */ - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(Client.class); - - /** - * Create a new Client. The client will not use authentication by default. - */ - public Client() { - client = new DefaultHttpClient(); - HttpParams params = client.getParams(); - params.setParameter("http.socket.timeout", - Integer.valueOf(DEFAULT_TIMEOUT)); - HttpHost proxyHost = (HttpHost) params - .getParameter(ConnRoutePNames.DEFAULT_PROXY); // XXX does this really work? - log.debug("proxy host: " + proxyHost.getHostName()); - log.debug("proxy port: " + proxyHost.getPort()); - doAuthentication = false; - } - - /** - * Initialise the server that will be used to send the network access. - * - * @param server server address/hostname - * @param port server port - */ - public void setServer(String server, int port) { - this.server = server; - this.port = port; - } - - /** - * Set the user credentials that will be used when making the access to the - * server. - * - * @param username The username. - * @param password The password. - */ - public void setCredentials(String username, String password) { - this.username = username; - this.password = password; - doAuthentication = true; - } - - /** - * Set the basic credentials. You must have previously set the server and - * port using setServer. - * - * @param username The username. - * @param password The password. - */ - private void setBasicCredentials(String username, String password) { - log.debug("server: " + server + " port: " + port + " u: '" + username - + "' p '" + password + "'"); - client.getCredentialsProvider().setCredentials(new AuthScope(server, port), - new UsernamePasswordCredentials(username, password)); - } - - /** - * Set a proxy that should be used by the client when trying to access the - * server. If this is not set, the client will attempt to make a direct - * direct connection to the server. The port is set to 80. - * - * @param host The hostname. - */ - public void setProxy(String host) { - setProxy(host, 80); - } - - /** - * Set a proxy that should be used by the client when trying to access the - * server. If this is not set, the client will attempt to make a direct - * direct connection to the server. - * - * @param host The name of the host. - * @param port The port. - */ - public void setProxy(String host, int port) { - client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, - new HttpHost(host, port)); // XXX does this really work? - } - - /** - * Clear the proxy setting. - */ - public void clearProxy() { - client.getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY); // XXX does this really work? - } - - /** - * Clear any user credentials that have been set for this client. - */ - public void clearCredentials() { - client.getCredentialsProvider().clear(); - doAuthentication = false; - } - - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; - } - - /** - * Set the connection timeout for the socket. - * - * @param milliseconds The time, expressed as a number of milliseconds. - */ - public void setSocketTimeout(int milliseconds) { - client.getParams().setParameter("http.socket.timeout", - Integer.valueOf(milliseconds)); - } - - /** - * Retrieve the service document. The service document is located at the - * specified URL. This calls getServiceDocument(url,onBehalfOf). - * - * @param url The location of the service document. - * @return The ServiceDocument, or null if there was a - * problem accessing the document. e.g. invalid access. - * @throws SWORDClientException If there is an error accessing the resource. - */ - public ServiceDocument getServiceDocument(String url) - throws SWORDClientException { - return getServiceDocument(url, null); - } - - /** - * Retrieve the service document. The service document is located at the - * specified URL. This calls getServiceDocument(url,onBehalfOf). - * - * @param url The location of the service document. - * @return The ServiceDocument, or null if there was a - * problem accessing the document. e.g. invalid access. - * @throws SWORDClientException If there is an error accessing the resource. - */ - public ServiceDocument getServiceDocument(String url, String onBehalfOf) - throws SWORDClientException { - URL serviceDocURL = null; - try { - serviceDocURL = new URL(url); - } catch (MalformedURLException e) { - // Try relative URL - URL baseURL = null; - try { - baseURL = new URL("http", server, Integer.valueOf(port), "/"); - serviceDocURL = new URL(baseURL, (url == null) ? "" : url); - } catch (MalformedURLException e1) { - // No dice, can't even form base URL... - throw new SWORDClientException(url + " is not a valid URL (" - + e1.getMessage() - + "), and could not form a relative one from: " - + baseURL + " / " + url, e1); - } - } - - HttpGet httpget = new HttpGet(serviceDocURL.toExternalForm()); - if (doAuthentication) { - // this does not perform any check on the username password. It - // relies on the server to determine if the values are correct. - setBasicCredentials(username, password); - } - - Properties properties = new Properties(); - - if (containsValue(onBehalfOf)) { - log.debug("Setting on-behalf-of: " + onBehalfOf); - httpget.addHeader(url, url); - httpget.addHeader(HttpHeaders.X_ON_BEHALF_OF, onBehalfOf); - properties.put(HttpHeaders.X_ON_BEHALF_OF, onBehalfOf); - } - - if (containsValue(userAgent)) { - log.debug("Setting userAgent: " + userAgent); - httpget.addHeader(HttpHeaders.USER_AGENT, userAgent); - properties.put(HttpHeaders.USER_AGENT, userAgent); - } - - ServiceDocument doc = null; - - try { - HttpResponse response = client.execute(httpget); - StatusLine statusLine = response.getStatusLine(); - int statusCode = statusLine.getStatusCode(); - // store the status code - status = new Status(statusCode, statusLine.getReasonPhrase()); - - if (status.getCode() == HttpStatus.SC_OK) { - String message = readResponse(response.getEntity().getContent()); - log.debug("returned message is: " + message); - doc = new ServiceDocument(); - lastUnmarshallInfo = doc.unmarshall(message, properties); - } else { - throw new SWORDClientException( - "Received error from service document request: " - + status); - } - } catch (IOException ioex) { - throw new SWORDClientException(ioex.getMessage(), ioex); - } catch (UnmarshallException uex) { - throw new SWORDClientException(uex.getMessage(), uex); - } finally { - httpget.releaseConnection(); - } - - return doc; - } - - private SwordValidationInfo lastUnmarshallInfo; - - /** - * @return SWORD validation info - */ - public SwordValidationInfo getLastUnmarshallInfo() { - return lastUnmarshallInfo; - } - - /** - * Post a file to the server. The different elements of the post are encoded - * in the specified message. - * - * @param message The message that contains the post information. - * @throws SWORDClientException if there is an error during the post operation. - */ - public DepositResponse postFile(PostMessage message) - throws SWORDClientException { - if (message == null) { - throw new SWORDClientException("Message cannot be null."); - } - - HttpPost httppost = new HttpPost(message.getDestination()); - - if (doAuthentication) { - setBasicCredentials(username, password); - } - - DepositResponse response = null; - - String messageBody = ""; - - try { - if (message.isUseMD5()) { - String md5 = ChecksumUtils.generateMD5(message.getFilepath()); - if (message.getChecksumError()) { - md5 = "1234567890"; - } - log.debug("checksum error is: " + md5); - if (md5 != null) { - httppost.addHeader(HttpHeaders.CONTENT_MD5, md5); - } - } - - String filename = message.getFilename(); - if (!"".equals(filename)) { - httppost.addHeader(HttpHeaders.CONTENT_DISPOSITION, - " filename=" + filename); - } - - if (containsValue(message.getSlug())) { - httppost.addHeader(HttpHeaders.SLUG, message.getSlug()); - } - - if (message.getCorruptRequest()) { - // insert a header with an invalid boolean value - httppost.addHeader(HttpHeaders.X_NO_OP, "Wibble"); - } else { - httppost.addHeader(HttpHeaders.X_NO_OP, Boolean - .toString(message.isNoOp())); - } - httppost.addHeader(HttpHeaders.X_VERBOSE, Boolean - .toString(message.isVerbose())); - - String packaging = message.getPackaging(); - if (packaging != null && packaging.length() > 0) { - httppost.addHeader(HttpHeaders.X_PACKAGING, packaging); - } - - String onBehalfOf = message.getOnBehalfOf(); - if (containsValue(onBehalfOf)) { - httppost.addHeader(HttpHeaders.X_ON_BEHALF_OF, onBehalfOf); - } - - String userAgent = message.getUserAgent(); - if (containsValue(userAgent)) { - httppost.addHeader(HttpHeaders.USER_AGENT, userAgent); - } - - - FileEntity requestEntity = new FileEntity( - new File(message.getFilepath()), - ContentType.create(message.getFiletype())); - httppost.setEntity(requestEntity); - - HttpResponse httpResponse = client.execute(httppost); - StatusLine statusLine = httpResponse.getStatusLine(); - int statusCode = statusLine.getStatusCode(); - status = new Status(statusCode, statusLine.getReasonPhrase()); - - log.info("Checking the status code: " + status.getCode()); - - if (status.getCode() == HttpStatus.SC_ACCEPTED - || status.getCode() == HttpStatus.SC_CREATED) { - messageBody = readResponse(httpResponse.getEntity().getContent()); - response = new DepositResponse(status.getCode()); - response.setLocation(httpResponse.getFirstHeader("Location").getValue()); - // added call for the status code. - lastUnmarshallInfo = response.unmarshall(messageBody, new Properties()); - } else { - messageBody = readResponse(httpResponse.getEntity().getContent()); - response = new DepositResponse(status.getCode()); - response.unmarshallErrorDocument(messageBody); - } - return response; - - } catch (NoSuchAlgorithmException nex) { - throw new SWORDClientException("Unable to use MD5. " - + nex.getMessage(), nex); - } catch (IOException ioex) { - throw new SWORDClientException(ioex.getMessage(), ioex); - } catch (UnmarshallException uex) { - throw new SWORDClientException(uex.getMessage() + "(
" + messageBody + "
)", uex); - } finally { - httppost.releaseConnection(); - } - } - - /** - * Read a response from the stream and return it as a string. - * - * @param stream The stream that contains the response. - * @return The string extracted from the screen. - * @throws UnsupportedEncodingException - * @throws IOException A general class of exceptions produced by failed or interrupted I/O - * operations. - */ - private String readResponse(InputStream stream) - throws UnsupportedEncodingException, IOException { - BufferedReader reader = new BufferedReader(new InputStreamReader( - stream, "UTF-8")); - String line = null; - StringBuffer buffer = new StringBuffer(); - while ((line = reader.readLine()) != null) { - buffer.append(line); - buffer.append("\n"); - } - return buffer.toString(); - } - - /** - * Return the status information that was returned from the most recent - * request sent to the server. - * - * @return The status code returned from the most recent access. - */ - public Status getStatus() { - return status; - } - - /** - * Check to see if the specified item contains a non-empty string. - * - * @param item The string to check. - * @return True if the string is not null and has a length greater than 0 - * after any whitespace is trimmed from the start and end. - * Otherwise, false. - */ - private boolean containsValue(String item) { - return ((item != null) && (item.trim().length() > 0)); - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ClientConstants.java b/dspace-sword/src/main/java/org/purl/sword/client/ClientConstants.java deleted file mode 100644 index 124ed38d33d8..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/ClientConstants.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * 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.purl.sword.client; - -/** - * Hold general constants for the client. - * - * @author Neil Taylor - */ -public class ClientConstants { - /** - * Current software version. - */ - public static final String CLIENT_VERSION = "1.1"; - - /** - * the name of this application - */ - public static final String SERVICE_NAME = "CASIS Test Client"; - - /** - * the name of this application - */ - public static final String NOT_DEFINED_TEXT = "Not defined"; - - /** - * The logging property file. - */ - public static final String LOGGING_PROPERTY_FILE = "log4j.properties"; - - /** - * Default constructor - */ - private ClientConstants() { } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ClientFactory.java b/dspace-sword/src/main/java/org/purl/sword/client/ClientFactory.java deleted file mode 100644 index 00bd9c17a541..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/ClientFactory.java +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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.purl.sword.client; - -/** - * Entry point for the SWORD Demonstration Client. This will parse the list of - * command line options and load either a Command Line client or a GUI client. - * - * @author Neil Taylor - */ -public class ClientFactory { - /** - * Generate a string that specifies the command line options for this - * program. - * - * @return A list of the options for this program. - */ - public static String usage() { - StringBuilder buffer = new StringBuilder(); - buffer.append("swordclient: version "); - buffer.append(ClientConstants.CLIENT_VERSION); - buffer.append("\n"); - - buffer.append("GUI Mode: "); - buffer.append("swordclient [-gui] [-nocapture]"); - buffer.append("\n\n"); - - buffer.append("Command Mode: Service - Request a Service Document\n"); - buffer.append("swordclient -cmd -t service [user-options] [proxy-options] -href url [-onBehalfOf name] "); - buffer.append("\n\n"); - - buffer.append("Command Mode: Post - Post a file to a remote service.\n"); - buffer.append("swordclient -cmd -t post [user-options] [proxy-options] [post-options] \n"); - buffer.append(" [-file file] [-filetype type] [-onBehalfOf name]"); - buffer.append("\n\n"); - - buffer.append("Command Mode: MultiPost - Post a file to multiple remote services.\n"); - buffer.append("swordclient -cmd -t multipost [user-options] [proxy-options] [post-options] \n"); - buffer.append(" [-dest dest]"); - - buffer.append("\n\n"); - buffer.append("User options: \n"); - buffer.append(" -u username Specify a username to access the remote service.\n"); - buffer.append(" -p password Specify a password to access the remote service.\n"); - buffer.append(" Required if -u option is used."); - - buffer.append("\n\n"); - buffer.append("Proxy options: \n"); - buffer.append(" -host host Hostname of a proxy, wwwproxy.aber.ac.uk.\n"); - buffer.append(" -port port Proxy port number, e.g. 8080.\n"); - - buffer.append("\n\n"); - buffer.append("Post options: \n"); - buffer.append(" -noOp Specified to indicate that the post is a test operation.\n"); - buffer.append(" -md5 Use an MD5 checksum in the message header.\n"); - buffer.append(" -checksumError Mis-calculate the file checksum for server test purposes.\n"); - buffer.append(" -formatNamespace ns The format namespace value.\n"); - buffer.append(" -slug name The slug value.\n"); - buffer.append(" -verbose Request a verbose response from the server.\n"); - - buffer.append("\n\n"); - buffer.append("Other options: \n"); - buffer.append(" -help Show this message.\n"); - buffer.append(" -t type The type of operation: service, post or multipost.\n"); - buffer.append(" -href url The URL for the service or post document.\n"); - buffer.append(" Required for service. The post and multipost operations \n"); - buffer.append(" will prompt you if the value is not provided.\n"); - buffer.append(" -filetype type The filetype, e.g. application/zip. The post and multipost\n"); - buffer.append(" will prompt you for the value if it is not provided.\n"); - buffer.append(" -onBehalfOf name Specify this parameter to set the On Behalf Of value.\n"); - buffer.append(" -dest dest Specify the destination for a deposit. This can be repeated\n"); - buffer.append(" multiple times. The format is: \n"); - buffer.append(" []:@\n"); - buffer.append(" e.g. sword[nst]:swordpass@http://sword.aber.ac.uk/post/\n"); - buffer.append(" nst:pass@http://sword.aber.ac.uk/post\n"); - buffer.append(" -nocapture Do not capture System.out and System.err to a debug panel\n"); - buffer.append(" in the GUI panel."); - - return buffer.toString(); - } - - /** - * Create a client. If GUI mode is set, a GUI client is created. Otherwise, - * a command line client is created. - * - * @param options The list of options extracted from the command line. - * @return A new client. - */ - public ClientType createClient(ClientOptions options) { - return new CmdClient(); - } - - /** - * Start the application and determine which client should be loaded. The - * application currently has two modes: GUI and client. The GUI mode is the - * default option. - * - * @param args the command line arguments given - */ - public static void main(String[] args) { - ClientFactory factory = new ClientFactory(); - - ClientOptions options = new ClientOptions(); - if (options.parseOptions(args)) { - ClientType client = factory.createClient(options); - client.run(options); - } else { - System.out.println(usage()); - } - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ClientOptions.java b/dspace-sword/src/main/java/org/purl/sword/client/ClientOptions.java deleted file mode 100644 index 1ffbfca022b2..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/ClientOptions.java +++ /dev/null @@ -1,605 +0,0 @@ -/** - * 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.purl.sword.client; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -import org.apache.commons.cli.ParseException; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * List of options that are parsed from the command line. - * - * @author Neil Taylor - */ -public class ClientOptions { - /** - * Label for the service operation. - */ - public static final String TYPE_SERVICE = "service"; - - /** - * Label for the post operation. - */ - public static final String TYPE_POST = "post"; - - /** - * Label for the multipost operation. - */ - public static final String TYPE_MULTI_POST = "multipost"; - - /** - * The access type. - */ - private String accessType = null; - - /** - * Proxy host name. - */ - private String proxyHost = null; - - /** - * Proxy host port. - */ - private int proxyPort = 8080; - - /** - * Username to access the service/post server. - */ - private String username = null; - - /** - * Password to access the service/post server. - */ - private String password = null; - - /** - * HREF of the server to access. - */ - private String href = null; - - /** - * Filename to post. - */ - private String filename = null; - - /** - * File type. - */ - private String filetype = null; - - /** - * Specifies that the output streams are not to be captured by the GUI client. - */ - private boolean noCapture = false; - - - /** - * SLUG Header field. - */ - private String slug = null; - - /** - * NoOp, used to indicate an operation on the server that does not - * require the file to be stored. - */ - private boolean noOp = false; - - /** - * Request verbose output from the server. - */ - private boolean verbose = false; - - /** - * OnBehalfOf user id. - */ - private String onBehalfOf = null; - - /** - * Format namespace to be used for the posted file. - */ - private String formatNamespace = null; - - /** - * Introduce a checksum error. This is used to simulate an error with the - * MD5 value. - */ - private boolean checksumError = false; - - /** - * Logger. - */ - private static final Logger log = LogManager.getLogger(); - - /** - * List of multiple destination items. Used if the mode is set to multipost. - */ - private final List multiPost = new ArrayList<>(); - - /** - * Pattern string to extract the data from a destination parameter in multipost mode. - */ - private static final Pattern MULTI_PATTERN - = Pattern.compile("(.*?)(\\[(.*?)\\]) {0,1}(:(.*)) {0,1}@(http://.*)"); - - /** - * Flag that indicates if the GUI mode has been set. This is - * true by default. - */ - private boolean guiMode = true; - - /** - * Flat that indicates if the MD5 option has been selected. This - * is true by default. - */ - private boolean md5 = false; - - /** - * Parse the list of options contained in the specified array. - * - * @param args The array of options. - * @return True if the options were parsed successfully. - */ - public boolean parseOptions(String[] args) { - Options options = new Options(); - options.addOption(Option.builder().longOpt("md5").build()) - .addOption(Option.builder().longOpt("noOp").build()) - .addOption(Option.builder().longOpt("verbose").build()) - .addOption(Option.builder().longOpt("cmd").build()) - .addOption(Option.builder().longOpt("gui").build()) - .addOption(Option.builder().longOpt("help").build()) - .addOption(Option.builder().longOpt("nocapture").build()); - - Option option; - - option = Option.builder().longOpt("host").hasArg().build(); - options.addOption(option); - - option = Option.builder().longOpt("port").hasArg().build(); - options.addOption(option); - - option = Option.builder("u").hasArg().build(); - options.addOption(option); - - option = Option.builder("p").hasArg().build(); - options.addOption(option); - - option = Option.builder().longOpt("href").hasArg().build(); - options.addOption(option); - - option = Option.builder("t").hasArg().build(); - options.addOption(option); - - option = Option.builder().longOpt("file").hasArg().build(); - options.addOption(option); - - option = Option.builder().longOpt("filetype").hasArg().build(); - options.addOption(option); - - option = Option.builder().longOpt("slug").hasArg().build(); - options.addOption(option); - - option = Option.builder().longOpt("onBehalfOf").hasArg().build(); - options.addOption(option); - - option = Option.builder().longOpt("formatNamespace").hasArg().build(); - options.addOption(option); - - option = Option.builder().longOpt("checksumError").build(); - options.addOption(option); - - option = Option.builder().longOpt("dest").hasArg().build(); - options.addOption(option); - - DefaultParser parser = new DefaultParser(); - CommandLine command; - try { - command = parser.parse(options, args); - } catch (ParseException ex) { - log.error(ex.getMessage()); - return false; - } - - if (command.hasOption("help")) { - return false; // force the calling code to display the usage information. - } - md5 = command.hasOption("md5"); - noOp = command.hasOption("noOp"); - verbose = command.hasOption("verbose"); - if (command.hasOption("cmd")) { - guiMode = false; - } - if (command.hasOption("gui")) { - guiMode = true; - } - proxyHost = command.getOptionValue("host"); - if (command.hasOption("port")) { - proxyPort = Integer.parseInt(command.getOptionValue("port")); - } - username = command.getOptionValue("u"); - password = command.getOptionValue("p"); - href = command.getOptionValue("href"); - accessType = command.getOptionValue("t"); - filename = command.getOptionValue("file"); - filetype = command.getOptionValue("filetype"); - slug = command.getOptionValue("slug"); - onBehalfOf = command.getOptionValue("onBehalfOf"); - formatNamespace = command.getOptionValue("formatNamespace"); - checksumError = command.hasOption("checksumError"); - noCapture = command.hasOption("nocapture"); - if (command.hasOption("dest")) { - String dest = command.getOptionValue("dest"); - Matcher m = MULTI_PATTERN.matcher(dest); - if (!m.matches()) { - log.debug("Error with dest parameter. Ignoring value: {}", dest); - } else { - int numGroups = m.groupCount(); - for (int g = 0; g <= numGroups; g++) { - log.debug("Group ({}) is: {}", g, m.group(g)); - } - - String group_username = m.group(1); - String group_onBehalfOf = m.group(3); - String group_password = m.group(5); - String group_url = m.group(6); - PostDestination destination = new PostDestination(group_url, - group_username, group_password, group_onBehalfOf); - - multiPost.add(destination); - } - } - - try { - // apply any settings - if (href == null && "service".equals(accessType)) { - log.error("No href specified."); - return false; - } - - if (multiPost.isEmpty() && "multipost".equals(accessType)) { - log.error("No destinations specified"); - return false; - } - - if (accessType == null && !guiMode) { - log.error("No access type specified"); - return false; - } - - if ((username == null && password != null) || (username != null && password == null)) { - log.error( - "The username and/or password are not specified. If one is specified, the other must also be " + - "specified."); - return false; - } - } catch (ArrayIndexOutOfBoundsException ex) { - log.error("Error with parameters."); - return false; - } - - return true; - } - - /** - * Get the access type. - * - * @return The value, or null if the value is not set. - */ - public String getAccessType() { - return accessType; - } - - /** - * Set the access type. - * - * @param accessType The value, or null to clear the value. - */ - public void setAccessType(String accessType) { - this.accessType = accessType; - } - - /** - * Get the proxy host. - * - * @return The value, or null if the value is not set. - */ - public String getProxyHost() { - return proxyHost; - } - - /** - * Set the proxy host. - * - * @param proxyHost The value, or null to clear the value. - */ - public void setProxyHost(String proxyHost) { - this.proxyHost = proxyHost; - } - - /** - * Get the proxy port. - * - * @return The proxy port. Default value is 80. - */ - public int getProxyPort() { - return proxyPort; - } - - /** - * Set the proxy port. - * - * @param proxyPort The proxy port. - */ - public void setProxyPort(int proxyPort) { - this.proxyPort = proxyPort; - } - - /** - * Get the username. - * - * @return The value, or null if the value is not set. - */ - public String getUsername() { - return username; - } - - /** - * Set the username. - * - * @param username The value, or null to clear the value. - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * Get the password. - * - * @return The value, or null if the value is not set. - */ - public String getPassword() { - return password; - } - - /** - * Set the password. - * - * @param password The value, or null to clear the value. - */ - public void setPassword(String password) { - this.password = password; - } - - /** - * Get the HREF of the service to access. - * - * @return The value, or null if the value is not set. - */ - public String getHref() { - return href; - } - - /** - * Set the HREF of the service to access. - * - * @param href The value, or null to clear the value. - */ - public void setHref(String href) { - this.href = href; - } - - /** - * Get the name of the file to post. - * - * @return The value, or null if the value is not set. - */ - public String getFilename() { - return filename; - } - - /** - * Set the name of the file to post. - * - * @param filename The value, or null to clear the value. - */ - public void setFilename(String filename) { - this.filename = filename; - } - - /** - * Get the type of the file to post. - * - * @return The filetype, or null if the value is not set. - */ - public String getFiletype() { - return filetype; - } - - /** - * Set the type of the file to post. - * - * @param filetype The value, or null to clear the value. - */ - public void setFiletype(String filetype) { - this.filetype = filetype; - } - - /** - * Determine if the tool is to be run in GUI mode. - * - * @return True if the tool is set for GUI mode. - */ - public boolean isGuiMode() { - return guiMode; - } - - /** - * Set the tool to run in GUI mode. - * - * @param guiMode True if the tool is to run in gui mode. - */ - public void setGuiMode(boolean guiMode) { - this.guiMode = guiMode; - } - - /** - * Get the MD5 setting. True if the tool is to use MD5 for post operations. - * - * @return The MD5 setting. - */ - public boolean isMd5() { - return md5; - } - - /** - * Set the MD5 setting. - * - * @param md5 True if the tool should use MD5 for post operations. - */ - public void setMd5(boolean md5) { - this.md5 = md5; - } - - /** - * Determine if the NoOp header should be sent. - * - * @return True if the header should be sent. - */ - public boolean isNoOp() { - return noOp; - } - - /** - * Set the NoOp setting. - * - * @param noOp True if the NoOp header should be used. - */ - public void setNoOp(boolean noOp) { - this.noOp = noOp; - } - - /** - * Determine if the verbose option is set. - * - * @return True if verbose option is set. - */ - public boolean isVerbose() { - return verbose; - } - - /** - * Set the verbose option. - * - * @param verbose True if verbose should be set. - */ - public void setVerbose(boolean verbose) { - this.verbose = verbose; - } - - /** - * Get the onBehalfOf value. - * - * @return The value, or null to clear the value. - */ - public String getOnBehalfOf() { - return onBehalfOf; - } - - /** - * Set the onBehalf of Value. - * - * @param onBehalfOf The value, or null to clear the value. - */ - public void setOnBehalfOf(String onBehalfOf) { - this.onBehalfOf = onBehalfOf; - } - - /** - * Get the format namespace value. - * - * @return The value, or null if the value is not set. - */ - public String getFormatNamespace() { - return formatNamespace; - } - - /** - * Set the format namespace value. - * - * @param formatNamespace The value, or null to clear the value. - */ - public void setFormatNamespace(String formatNamespace) { - this.formatNamespace = formatNamespace; - } - - /** - * Get the checksum error value. - * - * @return True if an error should be introduced into the checksum. - */ - public boolean getChecksumError() { - return checksumError; - } - - /** - * Set the checksum error value. - * - * @param checksumError True if the error should be introduced. - */ - public void setChecksumError(boolean checksumError) { - this.checksumError = checksumError; - } - - /** - * Get the current slug header. - * - * @return The slug value, or null if the value is not set. - */ - public String getSlug() { - return this.slug; - } - - /** - * Set the text that is to be used for the slug header. - * - * @param slug The value, or null to clear the value. - */ - public void setSlug(String slug) { - this.slug = slug; - } - - /** - * Get the list of post destinations. - * - * @return An iterator over the list of PostDestination objects. - */ - public Iterator getMultiPost() { - return multiPost.iterator(); - } - - - /** - * Determine if the noCapture option is set. This indicates that the code - * should not attempt to redirect stdout and stderr to a different output - * destination. Intended for use in a GUI client. - * - * @return The noCapture setting. True if set. - */ - public boolean isNoCapture() { - return noCapture; - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ClientType.java b/dspace-sword/src/main/java/org/purl/sword/client/ClientType.java deleted file mode 100644 index 11474782c1cf..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/ClientType.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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.purl.sword.client; - -/** - * Interface for a client. This contains a single method that allows the factory - * to pass a set of command line options to the client. - * - * @author Neil Taylor - */ -public interface ClientType { - /** - * Run the client, processing the specified options. - * - * @param options The options extracted from the command line. - */ - public void run(ClientOptions options); -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/CmdClient.java b/dspace-sword/src/main/java/org/purl/sword/client/CmdClient.java deleted file mode 100644 index 842e9d483dca..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/CmdClient.java +++ /dev/null @@ -1,445 +0,0 @@ -/** - * 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.purl.sword.client; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Iterator; -import java.util.List; - -import org.apache.logging.log4j.Logger; -import org.purl.sword.atom.Author; -import org.purl.sword.atom.Content; -import org.purl.sword.atom.Contributor; -import org.purl.sword.atom.Generator; -import org.purl.sword.atom.Link; -import org.purl.sword.atom.Rights; -import org.purl.sword.atom.Summary; -import org.purl.sword.atom.Title; -import org.purl.sword.base.Collection; -import org.purl.sword.base.DepositResponse; -import org.purl.sword.base.SWORDEntry; -import org.purl.sword.base.ServiceDocument; -import org.purl.sword.base.SwordAcceptPackaging; -import org.purl.sword.base.Workspace; - -/** - * Example implementation of a command line client. This can send out service - * document requests and print out the results and process posting a file to - * either a single or multiple destinations. The command line options are - * initialised prior to calling the class. The options are passed into the - * run(ClientOptions) method. - * - * @author Neil Taylor - */ -public class CmdClient implements ClientType { - /** - * The client that is used to process the service and post requests. - */ - private SWORDClient client; - - /** - * List of the options that can be specified on the command line. - */ - private ClientOptions options; - - /** - * The logger. - */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(CmdClient.class); - - /** - * Create a new instance of the class and create an instance of the - * client. - */ - public CmdClient() { - client = new Client(); - } - - /** - * Process the options that have been initialised from the command line. - * This will call one of service(), post() or multiPost(). - */ - public void process() { - if (options.getProxyHost() != null) { - client.setProxy(options.getProxyHost(), options.getProxyPort()); - } - - try { - String accessType = options.getAccessType(); - if (ClientOptions.TYPE_SERVICE.equals(accessType)) { - service(); - } else if (ClientOptions.TYPE_POST.equals(accessType)) { - post(); - } else if (ClientOptions.TYPE_MULTI_POST.equals(accessType)) { - System.out.println("checking multi-post"); - multiPost(); - } else { - System.out.println("Access type not recognised."); - } - - } catch (MalformedURLException mex) { - System.out - .println("The specified href was not valid: " + options.getHref() + " message: " + mex.getMessage()); - } catch (SWORDClientException ex) { - System.out.println("Exception: " + ex.getMessage()); - log.error("Unable to process request", ex); - } - } - - /** - * Process the service operation. Output the results of the service request. - * - * @throws SWORDClientException if there is an error processing the service request. - * @throws MalformedURLException if there is an error with the URL for the service request. - */ - private void service() - throws SWORDClientException, MalformedURLException { - String href = options.getHref(); - initialiseServer(href, options.getUsername(), options.getPassword()); - - ServiceDocument document = client.getServiceDocument(href, options.getOnBehalfOf()); - Status status = client.getStatus(); - System.out.println("The status is: " + status); - - if (status.getCode() == 200) { - log.debug("message is: " + document.marshall()); - - System.out.println("\nThe following Details were retrieved: "); - System.out.println("SWORD Version: " - + document.getService().getVersion()); - System.out.println("Supports NoOp? " + document.getService().isNoOp()); - System.out.println("Supports Verbose? " - + document.getService().isVerbose()); - System.out.println("Max Upload File Size " - + document.getService().getMaxUploadSize() + " kB"); - - Iterator workspaces = document.getService().getWorkspaces(); - for (; workspaces.hasNext(); ) { - Workspace workspace = workspaces.next(); - System.out.println("\nWorkspace Title: '" - + workspace.getTitle() + "'"); - - System.out.println("\n+ Collections ---"); - // process the collections - Iterator collections = workspace - .collectionIterator(); - for (; collections.hasNext(); ) { - Collection collection = collections.next(); - System.out.println("\nCollection location: " - + collection.getLocation()); - System.out.println("Collection title: " - + collection.getTitle()); - System.out - .println("Abstract: " + collection.getAbstract()); - System.out.println("Collection Policy: " - + collection.getCollectionPolicy()); - System.out.println("Treatment: " - + collection.getTreatment()); - System.out.println("Mediation: " - + collection.getMediation()); - - String[] accepts = collection.getAccepts(); - if (accepts != null && accepts.length == 0) { - System.out.println("Accepts: none specified"); - } else { - for (String s : accepts) { - System.out.println("Accepts: " + s); - } - } - List acceptsPackaging = collection.getAcceptPackaging(); - - StringBuilder acceptPackagingList = new StringBuilder(); - for (Iterator i = acceptsPackaging.iterator(); i.hasNext(); ) { - SwordAcceptPackaging accept = (SwordAcceptPackaging) i.next(); - acceptPackagingList.append(accept.getContent()).append(" (").append(accept.getQualityValue()) - .append("), "); - } - - System.out.println("Accepts Packaging: " + acceptPackagingList.toString()); - } - System.out.println("+ End of Collections ---"); - } - } - } - - /** - * Perform a post. If any of the destination URL, the filename and the - * filetype are missing, the user will be prompted to enter the values. - * - * @throws SWORDClientException if there is an error processing the post for a requested - * destination. - * @throws MalformedURLException if there is an error with the URL for the post. - */ - private void post() - throws SWORDClientException, MalformedURLException { - String url = options.getHref(); - if (url == null) { - url = readLine("Please enter the URL for the deposit: "); - } - - initialiseServer(url, options.getUsername(), options.getPassword()); - String file = options.getFilename(); - if (file == null) { - file = readLine("Please enter the filename to deposit: "); - } - String type = options.getFiletype(); - if (type == null) { - type = readLine("Please enter the file type, e.g. application/zip: "); - } - - PostMessage message = new PostMessage(); - message.setFilepath(file); - message.setDestination(url); - message.setFiletype(type); - message.setUseMD5(options.isMd5()); - message.setVerbose(options.isVerbose()); - message.setNoOp(options.isNoOp()); - message.setFormatNamespace(options.getFormatNamespace()); - message.setOnBehalfOf(options.getOnBehalfOf()); - message.setChecksumError(options.getChecksumError()); - message.setUserAgent(ClientConstants.SERVICE_NAME); - - processPost(message); - - } - - /** - * Perform a multi-post. Iterate over the list of -dest arguments in the command line - * options. For each -dest argument, attempt to post the file to the server. - * - * @throws SWORDClientException if there is an error processing the post for a requested - * destination. - * @throws MalformedURLException if there is an error with the URL for the post. - */ - private void multiPost() - throws SWORDClientException, MalformedURLException { - // request the common information - String file = options.getFilename(); - if (file == null) { - file = readLine("Please enter the filename to deposit: "); - } - String type = options.getFiletype(); - if (type == null) { - type = readLine("Please enter the file type, e.g. application/zip: "); - } - - // process this information for each of the specified destinations - PostDestination destination; - String url = null; - - Iterator iterator = options.getMultiPost(); - while (iterator.hasNext()) { - destination = iterator.next(); - url = destination.getUrl(); - initialiseServer(url, destination.getUsername(), destination.getPassword()); - - String onBehalfOf = destination.getOnBehalfOf(); - if (onBehalfOf == null) { - onBehalfOf = ""; - } else { - onBehalfOf = " on behalf of: " + onBehalfOf; - } - - System.out.println("Sending file to: " + url + " for: " + destination.getUsername() + - onBehalfOf); - PostMessage message = new PostMessage(); - message.setFilepath(file); - message.setDestination(url); - message.setFiletype(type); - message.setUseMD5(options.isMd5()); - message.setVerbose(options.isVerbose()); - message.setNoOp(options.isNoOp()); - message.setFormatNamespace(options.getFormatNamespace()); - message.setOnBehalfOf(destination.getOnBehalfOf()); - message.setChecksumError(options.getChecksumError()); - message.setUserAgent(ClientConstants.SERVICE_NAME); - - processPost(message); - } - - } - - /** - * Process the post response. The message contains the list of arguments - * for the post. The method will then print out the details of the - * response. - * - * @param message The post options. - * @throws SWORDClientException if there is an error accessing the - * post response. - */ - protected void processPost(PostMessage message) - throws SWORDClientException { - DepositResponse response = client.postFile(message); - - System.out.println("The status is: " + client.getStatus()); - - if (response != null) { - log.debug("message is: " + response.marshall()); - - // iterate over the data and output it - SWORDEntry entry = response.getEntry(); - - - System.out.println("Id: " + entry.getId()); - Title title = entry.getTitle(); - if (title != null) { - System.out.print("Title: " + title.getContent() + " type: "); - if (title.getType() != null) { - System.out.println(title.getType().toString()); - } else { - System.out.println("Not specified."); - } - } - - // process the authors - Iterator authors = entry.getAuthors(); - while (authors.hasNext()) { - Author author = authors.next(); - System.out.println("Author - " + author.toString()); - } - - Iterator categories = entry.getCategories(); - while (categories.hasNext()) { - System.out.println("Category: " + categories.next()); - } - - Iterator contributors = entry.getContributors(); - while (contributors.hasNext()) { - Contributor contributor = contributors.next(); - System.out.println("Contributor - " + contributor.toString()); - } - - Iterator links = entry.getLinks(); - while (links.hasNext()) { - Link link = links.next(); - System.out.println(link.toString()); - } - - Generator generator = entry.getGenerator(); - if (generator != null) { - System.out.println("Generator - " + generator.toString()); - } else { - System.out.println("There is no generator"); - } - - System.out.println("Published: " + entry.getPublished()); - - Content content = entry.getContent(); - if (content != null) { - System.out.println(content.toString()); - } else { - System.out.println("There is no content element."); - } - - Rights right = entry.getRights(); - if (right != null) { - System.out.println(right.toString()); - } else { - System.out.println("There is no right element."); - } - - Summary summary = entry.getSummary(); - if (summary != null) { - - System.out.println(summary.toString()); - } else { - System.out.println("There is no summary element."); - } - - System.out.println("Update: " + entry.getUpdated()); - System.out.println("Published: " + entry.getPublished()); - System.out.println("Verbose Description: " + entry.getVerboseDescription()); - System.out.println("Treatment: " + entry.getTreatment()); - System.out.println("Packaging: " + entry.getPackaging()); - - if (entry.isNoOpSet()) { - System.out.println("NoOp: " + entry.isNoOp()); - } - } else { - System.out.println("No valid Entry document was received from the server"); - } - } - - /** - * Initialise the server. Set the server that will be connected to and - * initialise any username and password. If the username and password are - * either null or contain empty strings, the user credentials will be cleared. - * - * @param location The location to connect to. This is a URL, of the format, - * http://a.host.com:port/. The host name and port number will - * be extracted. If the port is not specified, a default port of - * 80 will be used. - * @param username The username. If this is null or an empty string, the basic - * credentials will be cleared. - * @param password The password. If this is null or an empty string, the basic - * credentials will be cleared. - * @throws MalformedURLException if there is an error processing the URL. - */ - private void initialiseServer(String location, String username, String password) - throws MalformedURLException { - URL url = new URL(location); - int port = url.getPort(); - if (port == -1) { - port = 80; - } - - client.setServer(url.getHost(), port); - - if (username != null && username.length() > 0 && - password != null && password.length() > 0) { - log.info("Setting the username/password: " + username + " " - + password); - client.setCredentials(username, password); - } else { - client.clearCredentials(); - } - } - - /** - * Read a line of text from System.in. If there is an error reading - * from the input, the prompt will be redisplayed and the user asked - * to try again. - * - * @param prompt The prompt to display before the prompt. - * @return The string that is read from the line. - */ - private String readLine(String prompt) { - BufferedReader reader = new BufferedReader(new InputStreamReader( - System.in)); - String result = null; - - boolean ok = false; - while (!ok) { - try { - System.out.print(prompt); - System.out.flush(); - result = reader.readLine(); - ok = true; - } catch (IOException ex) { - System.out.println("There was an error with your input. Please try again."); - } - } - - return result; - } - - /** - * Run the client and process the specified options. - * - * @param options The command line options. - */ - public void run(ClientOptions options) { - this.options = options; - process(); - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/DebugOutputStream.java b/dspace-sword/src/main/java/org/purl/sword/client/DebugOutputStream.java deleted file mode 100644 index dfd8b6c60f26..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/DebugOutputStream.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * 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.purl.sword.client; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * A stream that will write any output to the specified panel. - * - * @author Neil Taylor - */ -public class DebugOutputStream extends OutputStream { - /** - * Panel that will display the messages. - */ - private MessageOutputPanel panel; - - /** - * Create a new instance and specify the panel that will receive the output. - * - * @param panel The panel. - */ - public DebugOutputStream(MessageOutputPanel panel) { - this.panel = panel; - } - - /** - * Override the write method from OutputStream. Capture the char and - * send it to the panel. - * - * @param arg0 The output character, expressed as an integer. - * @see java.io.OutputStream#write(int) - */ - public void write(int arg0) throws IOException { - panel.addCharacter(Character.valueOf((char) arg0)); - } - -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/MessageOutputPanel.java b/dspace-sword/src/main/java/org/purl/sword/client/MessageOutputPanel.java deleted file mode 100644 index e703f79ccccb..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/MessageOutputPanel.java +++ /dev/null @@ -1,138 +0,0 @@ -/** - * 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/ - */ - -/** - * Copyright (c) 2007, Aberystwyth University - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * - Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * - Neither the name of the Centre for Advanced Software and - * Intelligent Systems (CASIS) nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ -package org.purl.sword.client; - -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import javax.swing.JButton; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; - -/** - * Panel to display output messages. Text or characters can be sent to the - * panel for display. The panel also includes a button to clear any - * text that is currently displayed. - * - * @author Neil Taylor - */ -public class MessageOutputPanel extends JPanel - implements ActionListener { - - /** - * The text area that displays the messages. - */ - private JTextArea messages = null; - - /** - * Create a new instance and initialise the panel. - */ - public MessageOutputPanel() { - super(); - - setLayout(new GridBagLayout()); - - messages = new JTextArea(); - - JScrollPane detailsPane = new JScrollPane(messages, - JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, - JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); - JButton clearButton = new JButton("Clear"); - clearButton.addActionListener(this); - - //add components and set constraints - //dpc = details pane constraint - GridBagConstraints dpc = new GridBagConstraints(); - dpc.gridx = 0; - dpc.gridy = 0; - dpc.fill = GridBagConstraints.BOTH; - dpc.weightx = 0.75; - dpc.weighty = 0.45; - dpc.gridwidth = 2; - dpc.insets = new Insets(5, 5, 5, 5); - add(detailsPane, dpc); - - //cbc = clear button constraint - GridBagConstraints cbc = new GridBagConstraints(); - cbc.gridx = 1; - cbc.gridy = 1; - cbc.insets = new Insets(0, 0, 5, 5); - cbc.anchor = GridBagConstraints.LINE_END; - add(clearButton, cbc); - - } - - /** - * Add a message to the text area. The message will be added with a carriage return. - * - * @param message The message. - */ - public void addMessage(String message) { - messages.insert(message + "\n", messages.getDocument().getLength()); - } - - /** - * Add a single character to the text area. - * - * @param character The character. - */ - public void addCharacter(Character character) { - messages.insert(character.toString(), messages.getDocument().getLength()); - } - - /** - * Clear the text from the display. - * - * @param arg0 The action event. - * - * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent) - */ - public void actionPerformed(ActionEvent arg0) { - messages.setText(""); - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/PostDestination.java b/dspace-sword/src/main/java/org/purl/sword/client/PostDestination.java deleted file mode 100644 index 3124f02b45d7..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/PostDestination.java +++ /dev/null @@ -1,140 +0,0 @@ -/** - * 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.purl.sword.client; - -/** - * Details for a destination. This is used to represent a destination. If - * expressed as a string, the destination looks like: - *
- * <user>[<onBehalfOf>]:<password>@<url>
- * 
- * - * @author Neil Taylor - */ -public class PostDestination { - /** - * URL for the post destination. - */ - private String url; - - /** - * The username. - */ - private String username; - - /** - * The password. - */ - private String password; - - /** - * The onBehalfOf ID. - */ - private String onBehalfOf; - - /** - * Create a new instance. - */ - public PostDestination() { - // No-Op - } - - /** - * Create a new instance. - * - * @param url The url. - * @param username The username. - * @param password The password. - * @param onBehalfOf The onBehalfOf id. - */ - public PostDestination(String url, String username, String password, String onBehalfOf) { - this.url = url; - this.username = username; - this.password = password; - this.onBehalfOf = onBehalfOf; - } - - /** - * @return the url - */ - public String getUrl() { - return url; - } - - /** - * @param url the url to set - */ - public void setUrl(String url) { - this.url = url; - } - - /** - * @return the username - */ - public String getUsername() { - return username; - } - - /** - * @param username the username to set - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * @return the password - */ - public String getPassword() { - return password; - } - - /** - * @param password the password to set - */ - public void setPassword(String password) { - this.password = password; - } - - /** - * @return the onBehalfOf - */ - public String getOnBehalfOf() { - return onBehalfOf; - } - - /** - * @param onBehalfOf the onBehalfOf to set - */ - public void setOnBehalfOf(String onBehalfOf) { - this.onBehalfOf = onBehalfOf; - } - - /** - * Create a string representation of this object. - * - * @return The string. - */ - public String toString() { - StringBuffer buffer = new StringBuffer(); - buffer.append(username); - if (onBehalfOf != null) { - buffer.append("["); - buffer.append(onBehalfOf); - buffer.append("]"); - } - - if (password != null) { - buffer.append(":******"); - } - buffer.append("@"); - buffer.append(url); - - return buffer.toString(); - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/PostDialog.java b/dspace-sword/src/main/java/org/purl/sword/client/PostDialog.java deleted file mode 100644 index 84815b327e3b..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/PostDialog.java +++ /dev/null @@ -1,599 +0,0 @@ -/** - * 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/ - */ - -/** - * Copyright (c) 2007, Aberystwyth University - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * - Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * - Neither the name of the Centre for Advanced Software and - * Intelligent Systems (CASIS) nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ -package org.purl.sword.client; - -import java.awt.BorderLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; -import javax.swing.DefaultListModel; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JFileChooser; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JPasswordField; -import javax.swing.JScrollPane; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -/** - * Dialog for users to enter details of post destinations. - * - * @author Neil Taylor - */ -public class PostDialog - implements ActionListener, ChangeListener { - /** - * label for the browse command. - */ - protected static final String BROWSE = "browse"; - - /** - * label for the add command. - */ - protected static final String ADD = "add"; - - /** - * label for the edit command. - */ - protected static final String EDIT = "edit"; - - /** - * label for the delete command. - */ - protected static final String DELETE = "delete"; - - /** - * label for the clear command. - */ - protected static final String CLEAR = "clear"; - - /** - * Username combo box. - */ - private SWORDComboBox username; - - /** - * Post Location combo box. - */ - private SWORDComboBox postLocation; - - /** - * Password field. - */ - private JPasswordField password; - - /** - * The file combo box. - */ - private SWORDComboBox file; - - /** - * The filetype combo box. - */ - private SWORDComboBox fileType; - - /** - * The onBehalfOf combo box. - */ - private SWORDComboBox onBehalfOf; - - /** - * The md5 checkbox. - */ - private JCheckBox useMD5; - - /** - * The corruptMD5 checkbox. - */ - private JCheckBox corruptMD5; - - /** - * The corruptRequest checkbox. - */ - private JCheckBox corruptRequest; - - /** - * The useNoOp checkbox. - */ - private JCheckBox useNoOp; - - /** - * The verbose checkbox. - */ - private JCheckBox useVerbose; - - /** - * The format namespace combo box. - */ - private SWORDComboBox formatNamespace; - - /** - * The list of post destinations. - */ - private JList list; - - /** - * The parent frame for the dialog that is displayed. - */ - private JFrame parentFrame = null; - - /** - * Array that lists the labels for the buttons on the panel. - */ - private static Object[] options = {"Post File", "Cancel"}; - - /** - * The panel that holds the controls to show. - */ - private JPanel controls = null; - - /** - * - * @param parentFrame the parent of this dialog. - */ - public PostDialog(JFrame parentFrame) { - this.parentFrame = parentFrame; - controls = createControls(); - } - - /** - * Show the dialog with ok and cancel options. - * @return The return value from displaying JOptionPane. Either - * JOptionPane.OK_OPTION or JOptionPane.CANCEL_OPTION. - */ - public int show() { - int result = JOptionPane.showOptionDialog(parentFrame, - controls, - "Post Document", - JOptionPane.OK_CANCEL_OPTION, - JOptionPane.PLAIN_MESSAGE, - null, - options, - null); - - if (result == JOptionPane.OK_OPTION) { - // update the combo boxes with the values - username.updateList(); - file.updateList(); - fileType.updateList(); - onBehalfOf.updateList(); - formatNamespace.updateList(); - } - - return result; - } - - /** - * Create the controls for the main panel. - * - * @return The panel. - */ - protected final JPanel createControls() { - file = new SWORDComboBox(); - JPanel filePanel = new JPanel(new BorderLayout()); - filePanel.add(file, BorderLayout.CENTER); - JButton browse = new JButton("Browse..."); - browse.setActionCommand(BROWSE); - browse.addActionListener(this); - - filePanel.add(browse, BorderLayout.SOUTH); - - fileType = new SWORDComboBox(); - String type = "application/zip"; - fileType.addItem(type); - fileType.setSelectedItem(type); - - // controls that will be used in the second dialog - postLocation = new SWORDComboBox(); - username = new SWORDComboBox(); - password = new JPasswordField(); - onBehalfOf = new SWORDComboBox(); - - - useMD5 = new JCheckBox(); - useMD5.addChangeListener(this); - corruptMD5 = new JCheckBox(); - corruptRequest = new JCheckBox(); - useNoOp = new JCheckBox(); - useVerbose = new JCheckBox(); - formatNamespace = new SWORDComboBox(); - - JLabel fileLabel = new JLabel("File:", JLabel.TRAILING); - JLabel fileTypeLabel = new JLabel("File Type:", JLabel.TRAILING); - JLabel useMD5Label = new JLabel("Use MD5:", JLabel.TRAILING); - JLabel corruptMD5Label = new JLabel("Corrupt MD5:", JLabel.TRAILING); - JLabel corruptRequestLabel = new JLabel("Corrupt Request:", JLabel.TRAILING); - //JLabel corruptMD5Label = new JLabel("Corrupt MD5:", JLabel.TRAILING); - JLabel useNoOpLabel = new JLabel("Use noOp:", JLabel.TRAILING); - JLabel useVerboseLabel = new JLabel("Use verbose:", JLabel.TRAILING); - JLabel formatNamespaceLabel = new JLabel("X-Packaging:", JLabel.TRAILING); - JLabel userAgentLabel = new JLabel("User Agent:", JLabel.TRAILING); - JLabel userAgentNameLabel = new JLabel(ClientConstants.SERVICE_NAME, JLabel.LEADING); - - SWORDFormPanel panel = new SWORDFormPanel(); - panel.addFirstRow(new JLabel("Please enter the details for the post operation")); - - JPanel destinations = createDestinationsPanel(); - - panel.addRow(new JLabel("Destinations:"), destinations); - panel.addRow(fileLabel, filePanel); - panel.addRow(fileTypeLabel, fileType); - panel.addRow(useMD5Label, useMD5); - panel.addRow(corruptMD5Label, corruptMD5); - panel.addRow(corruptRequestLabel, corruptRequest); - panel.addRow(useNoOpLabel, useNoOp); - panel.addRow(useVerboseLabel, useVerbose); - panel.addRow(formatNamespaceLabel, formatNamespace); - panel.addRow(userAgentLabel, userAgentNameLabel); - - return panel; - } - - /** - * Create the destinations panel. This contains a list and four buttons - * to operate on values in the list. - * - * @return The panel containing the controls. - */ - protected JPanel createDestinationsPanel() { - DefaultListModel model = new DefaultListModel(); - list = new JList(model); - JScrollPane jsp = new JScrollPane(list); - - JPanel destinations = new JPanel(new BorderLayout()); - destinations.add(jsp, BorderLayout.CENTER); - JPanel destinationButtons = new JPanel(); - - JButton addButton = new JButton("Add"); - addButton.setActionCommand(ADD); - addButton.addActionListener(this); - - JButton editButton = new JButton("Edit"); - editButton.setActionCommand(EDIT); - editButton.addActionListener(this); - - JButton deleteButton = new JButton("Delete"); - deleteButton.setActionCommand(DELETE); - deleteButton.addActionListener(this); - - JButton clearButton = new JButton("Clear"); - clearButton.setActionCommand(CLEAR); - clearButton.addActionListener(this); - - destinationButtons.add(addButton); - destinationButtons.add(editButton); - destinationButtons.add(deleteButton); - destinationButtons.add(clearButton); - - destinations.add(destinationButtons, BorderLayout.SOUTH); - - return destinations; - } - - /** - * Handle the button click to select a file to upload. - */ - public void actionPerformed(ActionEvent evt) { - String cmd = evt.getActionCommand(); - - if (BROWSE.equals(cmd)) { - JFileChooser chooser = new JFileChooser(); - chooser.setCurrentDirectory(new File(System.getProperty("user.dir"))); - int returnVal = chooser.showOpenDialog(parentFrame); - if (returnVal == JFileChooser.APPROVE_OPTION) { - file.setSelectedItem(chooser.getSelectedFile().getAbsolutePath()); - } - } else if (ADD.equals(cmd)) { - PostDestination dest = showDestinationDialog(null); - if (dest != null) { - ((DefaultListModel) list.getModel()).addElement(dest); - } - } else if (EDIT.equals(cmd)) { - PostDestination dest = (PostDestination) list.getSelectedValue(); - if (dest != null) { - showDestinationDialog(dest); - list.repaint(); - } - } else if (DELETE.equals(cmd)) { - if (list.getSelectedIndex() != -1) { - ((DefaultListModel) list.getModel()).removeElementAt(list.getSelectedIndex()); - } - } else if (CLEAR.equals(cmd)) { - ((DefaultListModel) list.getModel()).clear(); - } - } - - /** - * Show the destination dialog. This is used to enter the URL, - * username, password and onBehalfOf name for a destination. - * - * @param destination The post destination. If this is not null, the values - * in the object are used to set the current values - * in the dialog controls. - * @return The post destination value. - */ - public PostDestination showDestinationDialog(PostDestination destination) { - SWORDFormPanel panel = new SWORDFormPanel(); - panel.addFirstRow(new JLabel("Please enter the details for the post operation")); - - JLabel postLabel = new JLabel("Post Location:", JLabel.TRAILING); - JLabel userLabel = new JLabel("Username:", JLabel.TRAILING); - JLabel passwordLabel = new JLabel("Password:", JLabel.TRAILING); - JLabel onBehalfOfLabel = new JLabel("On Behalf Of:", JLabel.TRAILING); - - panel.addRow(postLabel, postLocation); - panel.addRow(userLabel, username); - panel.addRow(passwordLabel, password); - panel.addRow(onBehalfOfLabel, onBehalfOf); - - if (destination != null) { - postLocation.insertItem(destination.getUrl()); - username.insertItem(destination.getUsername()); - password.setText(destination.getPassword()); - onBehalfOf.insertItem(destination.getOnBehalfOf()); - } else { - String s = ""; - postLocation.insertItem(s); - //postLocation.setSelectedItem(s); - username.insertItem(s); - username.setSelectedItem(s); - password.setText(s); - onBehalfOf.insertItem(s); - onBehalfOf.setSelectedItem(s); - } - - int result = JOptionPane.showOptionDialog(null, - panel, - "Destination", - JOptionPane.OK_CANCEL_OPTION, - JOptionPane.PLAIN_MESSAGE, - null, - new String[] {"OK", "Cancel"}, - null); - - if (result == JOptionPane.OK_OPTION) { - postLocation.updateList(); - username.updateList(); - onBehalfOf.updateList(); - - if (destination == null) { - destination = new PostDestination(); - } - - destination.setUrl(postLocation.getText()); - destination.setUsername(username.getText()); - String pass = new String(password.getPassword()); - if (pass.length() > 0) { - destination.setPassword(pass); - } else { - destination.setPassword(null); - } - - String obo = onBehalfOf.getText(); - if (obo.length() > 0) { - destination.setOnBehalfOf(onBehalfOf.getText()); - } else { - destination.setOnBehalfOf(null); - } - - } - - return destination; - } - - /** - * Get the list of Post Destinations. - * @return The destinations. - */ - public PostDestination[] getDestinations() { - DefaultListModel model = (DefaultListModel) list.getModel(); - PostDestination[] destinations = new PostDestination[model.size()]; - for (int i = 0; i < model.size(); i++) { - destinations[i] = (PostDestination) model.get(i); - } - return destinations; - } - - /** - * Get the file details. - * @return The value. - */ - public String getFile() { - return file.getText(); - } - - /** - * Get the filetype value. - * @return The value. - */ - public String getFileType() { - return fileType.getText(); - } - - /** - * Get the onBehalfOf value. - * @return The value. - */ - public String getOnBehalfOf() { - return onBehalfOf.getText(); - } - - /** - * Get the format namespace value. - * @return The value. - */ - public String getFormatNamespace() { - return formatNamespace.getText(); - } - - /** - * Determine if the MD5 checkbox is selected. - * - * @return True if the MD5 checkbox is selected. - */ - public boolean useMd5() { - return useMD5.isSelected(); - } - - /** - * Determine if the noOp checkbox is selected. - * - * @return True if the checkbox is selected. - */ - public boolean useNoOp() { - return useNoOp.isSelected(); - } - - /** - * Determine if the verbose checkbox is selected. - * - * @return True if the checkbox is selected. - */ - public boolean useVerbose() { - return useVerbose.isSelected(); - } - - /** - * Get the post location. - * @return The post location. - */ - public String getPostLocation() { - return postLocation.getText(); - } - - /** - * Determine if the MD5 hash should be corrupted. - * @return True if the corrupt MD5 checkbox is selected. The MD5 checkbox - * must also be selected. - */ - public boolean corruptMD5() { - return (corruptMD5.isEnabled() && corruptMD5.isSelected()); - } - - /** - * Determine if the POST request should be corrupted. - * @return True if the corrupt request checkbox is selected. - */ - public boolean corruptRequest() { - return (corruptRequest.isSelected()); - } - - /** - * Detect a state change event for the checkbox. - * - * @param evt The event. - */ - public void stateChanged(ChangeEvent evt) { - corruptMD5.setEnabled(useMD5.isSelected()); - } - - /** - * Add a list of user ids. - * - * @param users The user ids. - */ - public void addUserIds(String[] users) { - username.insertItems(users); - } - - /** - * Add a list of deposit URLs. - * - * @param deposits The URLs. - */ - public void addDepositUrls(String[] deposits) { - postLocation.insertItems(deposits); - } - - /** - * Add a list of onBehalfOf names. - * - * @param users The names. - */ - public void addOnBehalfOf(String[] users) { - onBehalfOf.insertItems(users); - } - - /** - * Add the list of formatNamespace strings. - * - * @param namespaces list of strings. - */ - public void addFormatNamespaces(String[] namespaces) { - formatNamespace.insertItems(namespaces); - } - - /** - * Add a list of file types. - * - * @param types The file types. - */ - public void addFileTypes(String[] types) { - fileType.insertItems(types); - } - - /** - * Add a list of file names. - * @param files The list of files. - */ - public void addFiles(String[] files) { - file.insertItems(files); - } - - /** - * Set the deposit location. - * - * @param location The location. - */ - public void setDepositLocation(String location) { - postLocation.insertItem(location); - postLocation.setSelectedItem(location); - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/PostMessage.java b/dspace-sword/src/main/java/org/purl/sword/client/PostMessage.java deleted file mode 100644 index a44f21ce2b88..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/PostMessage.java +++ /dev/null @@ -1,344 +0,0 @@ -/** - * 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/ - */ - -/** - * Copyright (c) 2007, Aberystwyth University - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * - Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * - Neither the name of the Centre for Advanced Software and - * Intelligent Systems (CASIS) nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ -package org.purl.sword.client; - -import java.io.File; - -/** - * Represents the details of a post to a server. The message holds all of the possible values - * that are to be sent from the client to the server. Not all elements of the message - * must be filled in. Any required fields are defined in the current SWORD specification. - * - * @author Neil Taylor - */ -public class PostMessage { - /** - * The local filepath for the file to upload/deposit. - */ - private String filepath; - - /** - * The URL of the destination server. - */ - private String destination; - - /** - * The filetype of the package that is to be uploaded. - */ - private String filetype; - - /** - * The string with the username if the deposit is on behalf of another user. - */ - private String onBehalfOf; - - /** - * True if an MD5 checksum should be sent with the deposit. - */ - private boolean useMD5; - - /** - * True if the deposit is a test and should not result in an actual deposit. - */ - private boolean noOp; - - /** - * True if the verbose operation is requested. - */ - private boolean verbose; - - /** - * The packaging format for the deposit. - */ - private String packaging; - - /** - * True if the deposit should simulate a checksum error. The client should check this - * field to determine if a correct MD5 checksum should be sent or whether the checksum should - * be modified so that it generates an error at the server. - */ - private boolean checksumError; - - /** - * True if the deposit should corrupt the POST header. The client should check this - * field to determine if a correct header should be sent or whether the header should - * be modified so that it generates an error at the server. - */ - private boolean corruptRequest; - - /** - * The Slug header value. - */ - private String slug; - - /** - * The user agent name - */ - private String userAgent; - - /** - * Get the filepath. - * - * @return The filepath. - */ - public String getFilepath() { - return filepath; - } - - /** - * Get the filename. This is the last element of the filepath - * that has been set in this class. - * - * @return filename - */ - public String getFilename() { - File file = new File(filepath); - return file.getName(); - } - - /** - * Set the filepath. - * - * @param filepath The filepath. - */ - public void setFilepath(String filepath) { - this.filepath = filepath; - } - - /** - * Get the destination collection. - * - * @return The collection. - */ - public String getDestination() { - return destination; - } - - /** - * Set the destination collection. - * - * @param destination The destination. - */ - public void setDestination(String destination) { - this.destination = destination; - } - - /** - * Get the filetype. - * @return The filetype. - */ - public String getFiletype() { - return filetype; - } - - /** - * Set the filetype. - * - * @param filetype The filetype. - */ - public void setFiletype(String filetype) { - this.filetype = filetype; - } - - /** - * Get the onBehalfOf value. - * - * @return The value. - */ - public String getOnBehalfOf() { - return onBehalfOf; - } - - /** - * Set the onBehalfOf value. - * - * @param onBehalfOf The value. - */ - public void setOnBehalfOf(String onBehalfOf) { - this.onBehalfOf = onBehalfOf; - } - - /** - * Get the MD5 status. - * @return The value. - */ - public boolean isUseMD5() { - return useMD5; - } - - /** - * Set the md5 state. - * - * @param useMD5 True if the message should use an MD5 checksum. - */ - public void setUseMD5(boolean useMD5) { - this.useMD5 = useMD5; - } - - /** - * Get the no-op state. - * - * @return The value. - */ - public boolean isNoOp() { - return noOp; - } - - /** - * Set the no-op state. - * - * @param noOp The no-op. - */ - public void setNoOp(boolean noOp) { - this.noOp = noOp; - } - - /** - * Get the verbose value. - * - * @return The value. - */ - public boolean isVerbose() { - return verbose; - } - - /** - * Set the verbose state. - * - * @param verbose True if the post message should send a - * verbose header. - */ - public void setVerbose(boolean verbose) { - this.verbose = verbose; - } - - /** - * Get the packaging format. - * - * @return The value. - */ - public String getPackaging() { - return packaging; - } - - /** - * Set the packaging format. - * - * @param packaging The packaging format. - */ - public void setFormatNamespace(String packaging) { - this.packaging = packaging; - } - - /** - * Get the status of the checksum error. - * - * @return True if the client should simulate a checksum error. - */ - public boolean getChecksumError() { - return checksumError; - } - - /** - * Set the state of the checksum error. - * - * @param checksumError True if the item should include a checksum error. - */ - public void setChecksumError(boolean checksumError) { - this.checksumError = checksumError; - } - - /** - * Get the status of the corrupt request flag. - * - * @return True if the client should corrupt the POST header. - */ - public boolean getCorruptRequest() { - return corruptRequest; - } - - /** - * Set the state of the corrupt request flag. - * - * @param corruptRequest True if the item should corrupt the POST header. - */ - public void setCorruptRequest(boolean corruptRequest) { - this.corruptRequest = corruptRequest; - } - - /** - * Set the Slug value. - * - * @param slug The value. - */ - public void setSlug(String slug) { - this.slug = slug; - } - - /** - * Get the Slug value. - * - * @return The Slug. - */ - public String getSlug() { - return this.slug; - } - - /** - * @return the userAgent - */ - public String getUserAgent() { - return userAgent; - } - - /** - * Set the user agent - * - * @param userAgent the userAgent to set - */ - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/PropertiesDialog.java b/dspace-sword/src/main/java/org/purl/sword/client/PropertiesDialog.java deleted file mode 100644 index 6d43cb68ef10..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/PropertiesDialog.java +++ /dev/null @@ -1,240 +0,0 @@ -/** - * 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.purl.sword.client; - -import java.awt.BorderLayout; -import java.util.Enumeration; -import java.util.Properties; -import javax.swing.DefaultCellEditor; -import javax.swing.JFrame; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableCellEditor; - -/** - * Dialog that is used to edit the collection of properties. - * - * @author Neil Taylor, Suzana Barreto - */ -public class PropertiesDialog { - /** - * The parent frame for the dialog that is displayed. - */ - private JFrame parentFrame = null; - - /** - * Array that lists the labels for the buttons on the panel. - */ - private static Object[] options = {"OK", "Cancel"}; - - /** - * The panel that holds the controls to show. - */ - private JPanel controls = null; - - /** - * The configuration properties - */ - private Properties properties = null; - - /** - * Table that is used to display the list of properties. - */ - private JTable propertiesTable; - - /** - * Create a new instance. - * - * @param parentFrame The parent frame for the dialog. - * @param props The properties lisst to display - */ - public PropertiesDialog(JFrame parentFrame, Properties props) { - this.parentFrame = parentFrame; - properties = props; - controls = createControls(); - } - - /** - * Create the controls that are to be displayed in the system. - * - * @return A panel that contains the controls. - */ - protected final JPanel createControls() { - JPanel panel = new JPanel(new BorderLayout()); - propertiesTable = new JTable(new PropertiesModel()); - ((DefaultCellEditor) propertiesTable.getDefaultEditor(String.class)).setClickCountToStart(1); - JScrollPane scrollpane = new JScrollPane(propertiesTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, - JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); - panel.add(scrollpane, BorderLayout.CENTER); - return panel; - } - - - /** - * Show the dialog and return the status code. - * - * @return The status code returned from the dialog. - */ - public int show() { - int result = JOptionPane.showOptionDialog(parentFrame, - controls, - "Edit Properties", - JOptionPane.OK_CANCEL_OPTION, - JOptionPane.PLAIN_MESSAGE, - null, - options, - null); - - // cancel any edit in the table. If there is a cell editing, the getEditingColumn will - // return a non-negative column number. This can be used to retreive the cell editor. - // The code then gets the default editor and calls the stopCellEditing. If custom - // editors are used, an additional check must be made to get the cell editor - // for a specific cell. - int column = propertiesTable.getEditingColumn(); - - if (column > -1) { - TableCellEditor editor = propertiesTable.getDefaultEditor(propertiesTable.getColumnClass(column)); - if (editor != null) { - editor.stopCellEditing(); - } - } - - return result; - } - - - /** - * A table model that is used to show the properties. The model links directly - * to the underlying properties object. As changes are made in the table, the - * corresponding changes are made in the properties object. The user can only - * edit the value column in the table. - */ - public class PropertiesModel extends AbstractTableModel { - /** - * Column names. - */ - private String columns[] = {"Property Name", "Value"}; - - /** - * Create a new instance of the model. If no properties object exists, - * a default model is created. Note, this will allow the table to - * continue editing, although this value will not be passed back to - * the calling window. - */ - public PropertiesModel() { - super(); - if (properties == null) { - properties = new Properties(); - } - } - - /** - * Get the number of columns. - * - * @return The number of columns. - */ - public int getColumnCount() { - return columns.length; - } - - /** - * Get the number of rows. - * - * @return The number of rows. - */ - public int getRowCount() { - return properties.size(); - } - - /** - * Get the value that is at the specified cell. - * - * @param row The row for the cell. - * @param col The column for the cell. - * @return The data value from the properties. - */ - public Object getValueAt(int row, int col) { - if (col == 0) { - return getKeyValue(row); - } else { - String key = getKeyValue(row); - return properties.get(key); - } - } - - /** - * Retrieve the column name for the specified column. - * - * @param col The column number. - * @return The column name. - */ - public String getColumnName(int col) { - return columns[col]; - } - - /** - * Retrieve the column class. - * - * @param col The column number. - * @return The class for the object found at the column position. - */ - public Class getColumnClass(int col) { - return getValueAt(0, col).getClass(); - } - - /** - * Determine if the cell can be edited. This model will only - * allow the second column to be edited. - * - * @param row The cell row. - * @param col The cell column. - * @return True if the cell can be edited. Otherwise, false. - */ - public boolean isCellEditable(int row, int col) { - if (col == 1) { - return true; - } - return false; - } - - /** - * Set the value for the specified cell. - * - * @param value The value to set. - * @param row The row for the cell. - * @param col The column. - */ - public void setValueAt(Object value, int row, int col) { - String key = getKeyValue(row); - properties.setProperty(key, ((String) value)); - fireTableCellUpdated(row, col); - } - - /** - * Get the Key value for the specified row. - * - * @param row The row. - * @return A string that shows the key value. - */ - public String getKeyValue(int row) { - int count = 0; - Enumeration k = properties.keys(); - while (k.hasMoreElements()) { - String key = (String) k.nextElement(); - if (count == row) { - return key; - } - count++; - } - return null; - } - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/SWORDClient.java b/dspace-sword/src/main/java/org/purl/sword/client/SWORDClient.java deleted file mode 100644 index 0df7fd53104a..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/SWORDClient.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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.purl.sword.client; - -import org.purl.sword.base.DepositResponse; -import org.purl.sword.base.ServiceDocument; - -/** - * Interface for any SWORD client implementation. - */ -public interface SWORDClient { - /** - * Set the server that is to be contacted on the next access. - * - * @param server The name of the server, e.g. www.aber.ac.uk - * @param port The port number, e.g. 80. - */ - public void setServer(String server, int port); - - /** - * Set the user credentials that are to be used for subsequent accesses. - * - * @param username The username. - * @param password The password. - */ - public void setCredentials(String username, String password); - - /** - * Clear the credentials settings on the client. - */ - public void clearCredentials(); - - /** - * Set the proxy that is to be used for subsequent accesses. - * - * @param host The host name, e.g. cache.host.com. - * @param port The port, e.g. 8080. - */ - public void setProxy(String host, int port); - - /** - * Get the status result returned from the most recent network test. - * - * @return The status code and message. - */ - public Status getStatus(); - - /** - * Get a service document, specified in the URL. - * - * @param url The URL to connect to. - * @return A ServiceDocument that contains the Service details that were - * obained from the specified URL. - * @throws SWORDClientException If there is an error accessing the - * URL. - */ - public ServiceDocument getServiceDocument(String url) throws SWORDClientException; - - /** - * Get a service document, specified in the URL. The document is accessed on - * behalf of the specified user. - * - * @param url The URL to connect to. - * @param onBehalfOf The username for the onBehalfOf access. - * @return A ServiceDocument that contains the Service details that were - * obtained from the specified URL. - * @throws SWORDClientException If there is an error accessing the URL. - */ - public ServiceDocument getServiceDocument(String url, String onBehalfOf) throws SWORDClientException; - - /** - * Post a file to the specified destination URL. - * - * @param message The message that defines the requirements for the operation. - * @return A DespoitResponse if the response is successful. If there was an error, - * null should be returned. - * @throws SWORDClientException If there is an error accessing the URL. - */ - public DepositResponse postFile(PostMessage message) throws SWORDClientException; -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/SWORDClientException.java b/dspace-sword/src/main/java/org/purl/sword/client/SWORDClientException.java deleted file mode 100644 index 1355b9d4ab61..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/SWORDClientException.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * 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.purl.sword.client; - -/** - * Represents an exception thrown by the SWORD Client. - * - * @author Neil Taylor - */ -public class SWORDClientException extends Exception { - /** - * Create a new exception, without a message. - */ - public SWORDClientException() { - super(); - } - - /** - * Create a new exception with the specified message. - * - * @param message The message. - */ - public SWORDClientException(String message) { - super(message); - } - - /** - * Create a new exception with the specified message and set - * the exception that generated this error. - * - * @param message The message. - * @param cause The original exception. - */ - public SWORDClientException(String message, Exception cause) { - super(message, cause); - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/SWORDComboBox.java b/dspace-sword/src/main/java/org/purl/sword/client/SWORDComboBox.java deleted file mode 100644 index 42fcc082ff58..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/SWORDComboBox.java +++ /dev/null @@ -1,129 +0,0 @@ -/** - * 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/ - */ - -/** - * Copyright (c) 2007, Aberystwyth University - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * - Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * - Neither the name of the Centre for Advanced Software and - * Intelligent Systems (CASIS) nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ -package org.purl.sword.client; - -import javax.swing.JComboBox; - -/** - * An extension of the JComboBox class. This adds a method that - * can update the list of items with the item. The update will only - * work on combo boxes that are set to editable. - * - * @author Neil Taylor - */ -public class SWORDComboBox extends JComboBox { - /** - * Create an instance of the SWORD Combo box. - */ - public SWORDComboBox() { - super(); - setEditable(true); - } - - /** - * Update the list for the Combo box with the currently selected - * item. This will only add an item to the list if: i) the control - * is editable, ii) the selected item is not empty and iii) the - * item is not already in the list. - */ - public void updateList() { - Object s = getSelectedItem(); - - if (!isEditable() || s == null || ((String) s).trim().length() == 0) { - // don't update with an empty item or if the combo box is not editable. - return; - } - - insertItem(s); - } - - /** - * Insert an item into the combo box. This will only be added - * if the item is not already present in the combo box. - * - * @param newItem The item to insert. - */ - public void insertItem(Object newItem) { - int count = getItemCount(); - - boolean found = false; - - for (int i = 0; i < count && !found; i++) { - Object item = getItemAt(i); - if (item != null && item.equals(newItem)) { - found = true; - } - } - - if (!found) { - addItem(newItem); - } - } - - /** - * Insert multiple items into the combo box. - * - * @param items The array of items. - */ - public void insertItems(String[] items) { - for (String item : items) { - insertItem(item); - } - } - - /** - * Get the text of the currently selected item in the combo box. - * @return The text. null is returned if no item - * is selected. - */ - public String getText() { - Object o = getSelectedItem(); - if (o != null) { - return o.toString().trim(); - } - - return null; - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/SWORDFormPanel.java b/dspace-sword/src/main/java/org/purl/sword/client/SWORDFormPanel.java deleted file mode 100644 index 81de59ae68c2..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/SWORDFormPanel.java +++ /dev/null @@ -1,144 +0,0 @@ -/** - * 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.purl.sword.client; - -import java.awt.Component; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.Insets; -import javax.swing.JPanel; - -/** - * Utility class. Creates a two column form. The left column is used to show - * the label for the row. The right column is used to show the control, e.g. - * text box, combo box or checkbox, for the row. - * - * @author Neil Taylor - */ -public class SWORDFormPanel extends JPanel { - /** - * Constraints used to control the layout on the panel. - */ - private GridBagConstraints labelConstraints; - - /** - * Constraints used to control the layout of the input controls on the panel. - */ - private GridBagConstraints controlConstraints; - - /** - * Index to the next row. - */ - private int rowIndex = 0; - - /** - * Insets for the top row of the label column. - */ - private Insets labelTop = new Insets(10, 10, 0, 0); - - /** - * Insets for the top row of the control column. - */ - private Insets controlTop = new Insets(10, 4, 0, 10); - - /** - * Insets for a general row in the label column. - */ - private Insets labelGeneral = new Insets(3, 10, 0, 0); - - /** - * Insets for a general row in the control column. - */ - private Insets controlGeneral = new Insets(3, 4, 0, 10); - - /** - * Create a new instance of the class. - */ - public SWORDFormPanel() { - super(); - setLayout(new GridBagLayout()); - - labelConstraints = new GridBagConstraints(); - labelConstraints.fill = GridBagConstraints.NONE; - labelConstraints.anchor = GridBagConstraints.LINE_END; - labelConstraints.weightx = 0.1; - - controlConstraints = new GridBagConstraints(); - controlConstraints.fill = GridBagConstraints.HORIZONTAL; - controlConstraints.weightx = 0.9; - } - - /** - * Add the specified component as the first row. It will occupy two - * columns. - * - * @param one The control to add. - */ - public void addFirstRow(Component one) { - addRow(one, null, labelTop, controlTop); - } - - /** - * Add the specified components as the first row in the form. - * - * @param one The label component. - * @param two The control component. - */ - public void addFirstRow(Component one, Component two) { - addRow(one, two, labelTop, controlTop); - } - - /** - * Add a component to the general row. This will be added in the label column. - * - * @param one The component. - */ - public void addRow(Component one) { - addRow(one, null); - } - - /** - * Add a component to the general row. - * - * @param one The component to add to the label column. - * @param two The component to add to the control column. - */ - public void addRow(Component one, Component two) { - addRow(one, two, labelGeneral, controlGeneral); - } - - /** - * Add a row to the table. - * - * @param one The component to display in the label column. - * @param two The component to display in the control column. - * @param labels The insets for the label column. - * @param controls The insets for the controls column. - */ - protected void addRow(Component one, Component two, Insets labels, Insets controls) { - labelConstraints.insets = labels; - labelConstraints.gridx = 0; - labelConstraints.gridy = rowIndex; - if (two == null) { - labelConstraints.gridwidth = 2; - } else { - labelConstraints.gridwidth = 1; - } - - add(one, labelConstraints); - - if (two != null) { - controlConstraints.insets = controls; - controlConstraints.gridx = 1; - controlConstraints.gridy = rowIndex; - add(two, controlConstraints); - } - - rowIndex++; - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ServiceDialog.java b/dspace-sword/src/main/java/org/purl/sword/client/ServiceDialog.java deleted file mode 100644 index 03489f0d2e77..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/ServiceDialog.java +++ /dev/null @@ -1,227 +0,0 @@ -/** - * 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/ - */ - -/** - * Copyright (c) 2007, Aberystwyth University - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * - Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * - Neither the name of the Centre for Advanced Software and - * Intelligent Systems (CASIS) nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ -package org.purl.sword.client; - -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JPasswordField; - -/** - * Dialog that prompts the user to enter the details for a service - * document location. - * - * @author Neil Taylor - */ -public class ServiceDialog { - /** - * The username. - */ - private SWORDComboBox username; - - /** - * The password. - */ - private JPasswordField password; - - /** - * Holds the URL for the collection. - */ - private SWORDComboBox location; - - /** - * The combo box that shows the list of onBehalfOf items. - */ - private SWORDComboBox onBehalfOf; - - /** - * Parent frame for the dialog. - */ - private JFrame parentFrame = null; - - /** - * The panel that holds the controls. - */ - private JPanel controls = null; - - /** - * List of buttons. - */ - private static Object[] options = {"Get Service Document", "Cancel"}; - - /** - * Create a new instance. - * - * @param parentFrame The parent frame. The dialog will be shown over the - * centre of this frame. - */ - public ServiceDialog(JFrame parentFrame) { - this.parentFrame = parentFrame; - controls = createControls(); - } - - /** - * Show the dialog. - * - * @return The close option. This is one of the dialog options from - * JOptionPane. - */ - public int show() { - int result = JOptionPane.showOptionDialog(parentFrame, - controls, - "Get Service Document", - JOptionPane.OK_CANCEL_OPTION, - JOptionPane.PLAIN_MESSAGE, - null, - options, - options[1]); - - if (result == JOptionPane.OK_OPTION) { - // update the combo boxes with the values - username.updateList(); - location.updateList(); - onBehalfOf.updateList(); - } - - return result; - } - - /** - * Create the controls that are displayed in the dialog. - * - * @return The panel that contains the controls. - */ - protected final JPanel createControls() { - username = new SWORDComboBox(); - username.setEditable(true); - password = new JPasswordField(); - location = new SWORDComboBox(); - location.setEditable(true); - onBehalfOf = new SWORDComboBox(); - onBehalfOf.setEditable(true); - - JLabel userLabel = new JLabel("Username:", JLabel.TRAILING); - JLabel passwordLabel = new JLabel("Password:", JLabel.TRAILING); - JLabel locationLabel = new JLabel("Location:", JLabel.TRAILING); - JLabel onBehalfOfLabel = new JLabel("On Behalf Of:", JLabel.TRAILING); - - SWORDFormPanel panel = new SWORDFormPanel(); - panel.addFirstRow(userLabel, username); - panel.addRow(passwordLabel, password); - panel.addRow(locationLabel, location); - panel.addRow(onBehalfOfLabel, onBehalfOf); - - return panel; - } - - /** - * Get the username from the controls on the dialog. - * - * @return The username. - */ - public String getUsername() { - return username.getText(); - } - - /** - * Get the password from the dialog. - * - * @return The password. - */ - public String getPassword() { - return new String(password.getPassword()); - } - - /** - * The location from the dialog. - * - * @return The location. - */ - public String getLocation() { - return location.getText(); - } - - /** - * The onBehalfOf value from the dialog. - * - * @return The onBehalfOf value. - */ - public String getOnBehalfOf() { - String text = onBehalfOf.getText().trim(); - if (text.length() == 0) { - return null; - } - return text; - } - - /** - * Add the list of user ids to the dialog. - * - * @param users The list of user ids. - */ - public void addUserIds(String[] users) { - username.insertItems(users); - } - - /** - * Add the list of service URLs. - * - * @param services The service URLs. - */ - public void addServiceUrls(String[] services) { - location.insertItems(services); - } - - /** - * Add a list of onBehalfOf names. - * - * @param users The list of onBehalfOf items. - */ - public void addOnBehalfOf(String[] users) { - onBehalfOf.insertItems(users); - } - -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ServicePanel.java b/dspace-sword/src/main/java/org/purl/sword/client/ServicePanel.java deleted file mode 100644 index bc3031c33354..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/ServicePanel.java +++ /dev/null @@ -1,843 +0,0 @@ -/** - * 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/ - */ - -/** - * Copyright (c) 2007, Aberystwyth University - * - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * - Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * - Neither the name of the Centre for Advanced Software and - * Intelligent Systems (CASIS) nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ -package org.purl.sword.client; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; -import javax.swing.Icon; -import javax.swing.ImageIcon; -import javax.swing.JComponent; -import javax.swing.JEditorPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSplitPane; -import javax.swing.JTree; -import javax.swing.ToolTipManager; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeCellRenderer; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreePath; - -import org.purl.sword.atom.Author; -import org.purl.sword.atom.Content; -import org.purl.sword.atom.Contributor; -import org.purl.sword.atom.Generator; -import org.purl.sword.atom.Link; -import org.purl.sword.atom.TextConstruct; -import org.purl.sword.base.Collection; -import org.purl.sword.base.DepositResponse; -import org.purl.sword.base.SWORDEntry; -import org.purl.sword.base.Service; -import org.purl.sword.base.ServiceDocument; -import org.purl.sword.base.SwordAcceptPackaging; -import org.purl.sword.base.Workspace; - -/** - * The main panel for the GUI client. This contains the top-two sub-panels: the - * tree and the text area to show the details of the selected node. - * - * @author Neil Taylor - */ -public class ServicePanel extends JPanel - implements TreeSelectionListener { - /** - * The top level item in the tree that lists services. - */ - DefaultMutableTreeNode top; - - /** - * The tree model used to display the items. - */ - DefaultTreeModel treeModel = null; - - /** - * Tree that holds the list of services. - */ - private JTree services; - - /** - * The panel that shows an HTML table with any details for the selected - * node in the services tree. - */ - private JEditorPane details; - - /** - * A registered listener. This listener will be notified when there is a - * different node selected in the service tree. - */ - private ServiceSelectedListener listener; - - /** - * Create a new instance of the panel. - */ - public ServicePanel() { - super(); - setLayout(new BorderLayout()); - - top = new DefaultMutableTreeNode("Services & Posted Files"); - treeModel = new DefaultTreeModel(top); - - services = new JTree(treeModel); - services.setCellRenderer(new ServicePostTreeRenderer()); - - JScrollPane servicesPane = new JScrollPane(services, - JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, - JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); - - details = new JEditorPane("text/html", - "

Details

This panel will show the details for the currently " + - "selected item in the tree.

"); - - JScrollPane detailsPane = new JScrollPane(details, - JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, - JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); - - JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, - servicesPane, - detailsPane); - splitPane.setOneTouchExpandable(true); - splitPane.setResizeWeight(0.5); - splitPane.setDividerLocation(200); - - services.addTreeSelectionListener(this); - ToolTipManager.sharedInstance().registerComponent(services); - - add(splitPane, BorderLayout.CENTER); - } - - /** - * Renderer that displays the icons for the tree nodes. - * - * @author Neil Taylor - */ - static class ServicePostTreeRenderer extends DefaultTreeCellRenderer { - Icon workspaceIcon; - Icon serviceIcon; - Icon collectionIcon; - Icon fileIcon; - - /** - * Initialise the renderer. Load the icons. - */ - public ServicePostTreeRenderer() { - ClassLoader loader = this.getClass().getClassLoader(); - workspaceIcon = new ImageIcon(loader.getResource("images/WorkspaceNodeImage.gif")); - serviceIcon = new ImageIcon(loader.getResource("images/ServiceNodeImage.gif")); - collectionIcon = new ImageIcon(loader.getResource("images/CollectionNodeImage.gif")); - fileIcon = new ImageIcon(loader.getResource("images/ServiceNodeImage.gif")); - } - - /** - * Return the cell renderer. This will be the default tree cell renderer - * with a different icon depending upon the type of data in the node. - * - * @param tree The JTree control. - * @param value The value to display. - * @param sel True if the node is selected. - * @param expanded True if the node is expanded. - * @param leaf True if the node is a leaf. - * @param row The row. - * @param hasFocus True if the node has focus. - */ - public Component getTreeCellRendererComponent( - JTree tree, - Object value, - boolean sel, - boolean expanded, - boolean leaf, - int row, - boolean hasFocus) { - - JComponent comp = (JComponent) super.getTreeCellRendererComponent( - tree, value, sel, - expanded, leaf, row, - hasFocus); - - DefaultMutableTreeNode node = - (DefaultMutableTreeNode) value; - - Object o = node.getUserObject(); - if (o instanceof TreeNodeWrapper) { - TreeNodeWrapper wrapper = (TreeNodeWrapper) o; - comp.setToolTipText(wrapper.toString()); - Object data = wrapper.getData(); - if (data instanceof Service) { - setIcon(serviceIcon); - } else if (data instanceof Workspace) { - setIcon(workspaceIcon); - } else if (data instanceof Collection) { - setIcon(collectionIcon); - } else if (data instanceof SWORDEntry) { - setIcon(fileIcon); - } - } else { - comp.setToolTipText(null); - } - return comp; - } - - - } - - /** - * Set the service selected listener. This listener will be notified when - * there is a selection change in the tree. - * - * @param listener The listener. - */ - public void setServiceSelectedListener(ServiceSelectedListener listener) { - this.listener = listener; - } - - /** - * Process the specified service document. Add the details as a new child of the - * root of the tree. - * - * @param url The url used to access the service document. - * @param doc The service document. - */ - public void processServiceDocument(String url, - ServiceDocument doc) { - TreeNodeWrapper wrapper = null; - - Service service = doc.getService(); - wrapper = new TreeNodeWrapper(url, service); - DefaultMutableTreeNode serviceNode = new DefaultMutableTreeNode(wrapper); - treeModel.insertNodeInto(serviceNode, top, top.getChildCount()); - services.scrollPathToVisible(new TreePath(serviceNode.getPath())); - - // process the workspaces - DefaultMutableTreeNode workspaceNode = null; - - Iterator workspaces = service.getWorkspaces(); - for (; workspaces.hasNext(); ) { - Workspace workspace = workspaces.next(); - wrapper = new TreeNodeWrapper(workspace.getTitle(), workspace); - workspaceNode = new DefaultMutableTreeNode(wrapper); - treeModel.insertNodeInto(workspaceNode, serviceNode, serviceNode.getChildCount()); - services.scrollPathToVisible(new TreePath(workspaceNode.getPath())); - - DefaultMutableTreeNode collectionNode = null; - Iterator collections = workspace.collectionIterator(); - for (; collections.hasNext(); ) { - Collection collection = collections.next(); - wrapper = new TreeNodeWrapper(collection.getTitle(), collection); - collectionNode = new DefaultMutableTreeNode(wrapper); - treeModel.insertNodeInto(collectionNode, workspaceNode, workspaceNode.getChildCount()); - services.scrollPathToVisible(new TreePath(collectionNode.getPath())); - } - } // for - } - - /** - * Holds the data for a tree node. It specifies the name that will be displayed - * in the node, and stores associated data. - * - * @author Neil Taylor - */ - static class TreeNodeWrapper { - /** - * The node name. - */ - private String name; - - /** - * The user data. - */ - private Object userObject; - - /** - * Create a new instance. - * - * @param name The name of the node. - * @param data The data in the node. - */ - public TreeNodeWrapper(String name, Object data) { - this.name = name; - this.userObject = data; - } - - /** - * Retrieve the data that is stored in this node. - * - * @return The data. - */ - public Object getData() { - return userObject; - } - - /** - * Get a string description for this node. - */ - public String toString() { - if (name == null || name.trim().equals("")) { - return "Unspecified"; - } - - return name; - } - } - - /** - * Respond to a changed tree selection event. Update the details panel to - * show an appropriate message for the newly selected node. Also, - * alert the selection listener for this panel. The listener will receive - * a path, if a collection has been selected. Otherwise, the listener - * will receive null. - */ - public void valueChanged(TreeSelectionEvent evt) { - // Get all nodes whose selection status has changed - TreePath[] paths = evt.getPaths(); - - for (int i = 0; i < paths.length; i++) { - if (evt.isAddedPath(i)) { - // process new selections - DefaultMutableTreeNode node; - node = (DefaultMutableTreeNode) (paths[i].getLastPathComponent()); - - Object o = node.getUserObject(); - if (o instanceof TreeNodeWrapper) { - try { - TreeNodeWrapper wrapper = (TreeNodeWrapper) o; - Object data = wrapper.getData(); - if (data instanceof Service) { - showService((Service) data); - alertListener(null); - } else if (data instanceof Workspace) { - showWorkspace((Workspace) data); - if (listener != null) { - alertListener(null); - } - } else if (data instanceof Collection) { - Collection c = (Collection) data; - showCollection(c); - alertListener(c.getLocation()); - } else if (data instanceof SWORDEntry) { - showEntry((SWORDEntry) data); - alertListener(null); - } else { - details.setText("unknown"); - alertListener(null); - } - } catch (Exception e) { - details.setText( - "An error occurred. The message was: " + e.getMessage() + ""); - alertListener(null); - e.printStackTrace(); - } - } else { - details.setText("please select one of the other nodes"); - alertListener(null); - } - } - - } - } - - /** - * Notify the listener that there has been a change to the currently selected - * item in the tree. - * - * @param value The value to send to the listener. - */ - private void alertListener(String value) { - if (listener != null) { - listener.selected(value); - } - } - - /** - * Add a new HTML table row to the specified StringBuffer. The label is displayed in - * the left column and the value is displayed in the right column. - * - * @param buffer The destination string buffer. - * @param label The label to add. - * @param value The corresponding value to add. - */ - private void addTableRow(StringBuffer buffer, String label, Object value) { - buffer.append(""); - buffer.append(label); - buffer.append(""); - buffer.append(displayableValue(value)); - buffer.append(""); - } - - /** - * Show the specified service data in the details panel. - * - * @param service The service node to display. - */ - private void showService(Service service) { - StringBuffer buffer = new StringBuffer(); - buffer.append(""); - buffer.append(""); - - buffer.append(""); - buffer.append(""); - addTableRow(buffer, "SWORD Version", service.getVersion()); - addTableRow(buffer, "NoOp Support ", service.isNoOp()); - addTableRow(buffer, "Verbose Support ", service.isVerbose()); - - String maxSize = ""; - - // Commented out the following code as the client code is out of step with the - // Sword 'base' library and wont compile. - Robin Taylor. - //if ( service.maxUploadIsDefined() ) - //{ - // maxSize = "" + service.getMaxUploadSize() + "kB"; - //} - //else - //{ - maxSize = "undefined"; - //} - - addTableRow(buffer, "Max File Upload Size ", maxSize); - - buffer.append("
Service Summary
"); - - buffer.append(""); - buffer.append(""); - details.setText(buffer.toString()); - } - - /** - * Display the workspace data in the details panel. - * - * @param workspace The workspace. - */ - private void showWorkspace(Workspace workspace) { - StringBuffer buffer = new StringBuffer(); - buffer.append(""); - buffer.append(""); - - buffer.append(""); - buffer - .append(""); - addTableRow(buffer, "Workspace Title", workspace.getTitle()); - buffer.append("
Workspace Summary
"); - - buffer.append(""); - buffer.append(""); - details.setText(buffer.toString()); - } - - /** - * Return the parameter unmodified if set, or the not defined text if null - * @param s - * @return s or ClientConstants.NOT_DEFINED_TEXT - */ - private Object displayableValue(Object s) { - if (null == s) { - return ClientConstants.NOT_DEFINED_TEXT; - } else { - return s; - } - } - - /** - * Add a string within paragraph tags. - * - * @param buffer The buffer to add the message to. - * @param message The message to add. - */ - private void addPara(StringBuffer buffer, String message) { - buffer.append("

" + message + "

"); - } - - /** - * Show the specified collection data in the details panel. - * - * @param collection The collection data. - */ - private void showCollection(Collection collection) { - StringBuffer buffer = new StringBuffer(); - buffer.append(""); - buffer.append(""); - - if (collection == null) { - addPara(buffer, "Invalid Collection object. Unable to display details."); - } else { - buffer.append(""); - buffer.append( - ""); - addTableRow(buffer, "Collection location", collection.getLocation()); - addTableRow(buffer, "Collection title", collection.getTitle()); - addTableRow(buffer, "Abstract", collection.getAbstract()); - addTableRow(buffer, "Collection Policy", collection.getCollectionPolicy()); - addTableRow(buffer, "Treatment", collection.getTreatment()); - addTableRow(buffer, "Mediation", collection.getMediation()); - addTableRow(buffer, "Nested Service Document", collection.getService()); - - String[] accepts = collection.getAccepts(); - StringBuilder acceptList = new StringBuilder(); - if (accepts != null && accepts.length == 0) { - acceptList.append("None specified"); - } else { - for (String s : accepts) { - acceptList.append(s).append("
"); - } - } - addTableRow(buffer, "Accepts", acceptList.toString()); - - List acceptsPackaging = collection.getAcceptPackaging(); - - StringBuilder acceptPackagingList = new StringBuilder(); - for (Iterator i = acceptsPackaging.iterator(); i.hasNext(); ) { - SwordAcceptPackaging accept = (SwordAcceptPackaging) i.next(); - acceptPackagingList.append(accept.getContent()).append(" (").append(accept.getQualityValue()) - .append(")"); - - // add a , separator if there are any more items in the list - if (i.hasNext()) { - acceptPackagingList.append(", "); - } - } - - addTableRow(buffer, "Accepts Packaging", acceptPackagingList.toString()); - - buffer.append("
Collection Summary
"); - } - - buffer.append(""); - buffer.append(""); - details.setText(buffer.toString()); - } - - /** - * Display the contents of a Post entry in the display panel. - * - * @param entry The entry to display. - */ - private void showEntry(SWORDEntry entry) { - StringBuffer buffer = new StringBuffer(); - buffer.append(""); - buffer.append(""); - - if (entry == null) { - addPara(buffer, "Invalid Entry object. Unable to display details."); - } else { - buffer.append(""); - buffer - .append(""); - - // process atom:title - String titleString = getTextConstructDetails(entry.getSummary()); - addTableRow(buffer, "Title", titleString); - - // process id - addTableRow(buffer, "ID", entry.getId()); - - // process updated - addTableRow(buffer, "Date Updated", entry.getUpdated()); - - String authorString = getAuthorDetails(entry.getAuthors()); - addTableRow(buffer, "Authors", authorString); - - // process summary - String summaryString = getTextConstructDetails(entry.getSummary()); - addTableRow(buffer, "Summary", summaryString); - - // process content - Content content = entry.getContent(); - String contentString = ""; - if (content == null) { - contentString = "Not defined."; - } else { - contentString += "Source: '" + content.getSource() + "', Type: '" + - content.getType() + "'"; - } - addTableRow(buffer, "Content", contentString); - - // process links - Iterator links = entry.getLinks(); - StringBuffer linkBuffer = new StringBuffer(); - for (; links.hasNext(); ) { - Link link = links.next(); - linkBuffer.append("href: '"); - linkBuffer.append(link.getHref()); - linkBuffer.append("', href lang: '"); - linkBuffer.append(link.getHreflang()); - linkBuffer.append("', rel: '"); - linkBuffer.append(link.getRel()); - linkBuffer.append("')
"); - } - if (linkBuffer.length() == 0) { - linkBuffer.append("Not defined"); - } - addTableRow(buffer, "Links", linkBuffer.toString()); - - // process contributors - String contributorString = getContributorDetails(entry.getContributors()); - addTableRow(buffer, "Contributors", contributorString); - - // process source - String sourceString = ""; - Generator generator = entry.getGenerator(); - if (generator != null) { - sourceString += "Content: '" + generator.getContent() + "'
'"; - sourceString += "Version: '" + generator.getVersion() + "'
'"; - sourceString += "Uri: '" + generator.getUri() + "'"; - } else { - sourceString += "No generator defined."; - } - - addTableRow(buffer, "Generator", sourceString); - - // process treatment - addTableRow(buffer, "Treatment", entry.getTreatment()); - - // process verboseDescription - addTableRow(buffer, "Verbose Description", entry.getVerboseDescription()); - - // process noOp - addTableRow(buffer, "NoOp", entry.isNoOp()); - - // process formatNamespace - addTableRow(buffer, "Packaging", entry.getPackaging()); - - // process userAgent - addTableRow(buffer, "User Agent", entry.getUserAgent()); - - - buffer.append("
Entry Summary
"); - } - - buffer.append(""); - buffer.append(""); - details.setText(buffer.toString()); - } - - /** - * Retrieve the details for a TextConstruct object. - * - * @param data The text construct object to display. - * - * @return Either 'Not defined' if the data is null, or - * details of the text content element. - */ - private String getTextConstructDetails(TextConstruct data) { - String summaryStr = ""; - if (data == null) { - summaryStr = "Not defined"; - } else { - summaryStr = "Content: '" + data.getContent() + "', Type: "; - if (data.getType() != null) { - summaryStr += "'" + data.getType().toString() + "'"; - } else { - summaryStr += "undefined."; - } - } - - return summaryStr; - } - - /** - * Get the author details and insert them into a string. - * - * @param authors the list of authors to process. - * - * @return A string containing the list of authors. - */ - private String getAuthorDetails(Iterator authors) { - // process author - StringBuffer authorBuffer = new StringBuffer(); - for (; authors.hasNext(); ) { - Author a = authors.next(); - authorBuffer.append(getAuthorDetails(a)); - } - - if (authorBuffer.length() == 0) { - authorBuffer.append("Not defined"); - } - - return authorBuffer.toString(); - } - - /** - * Get the contributor details and insert them into a string. - * - * @param contributors The contributors. - * - * @return The string that lists the details of the contributors. - */ - private String getContributorDetails(Iterator contributors) { - // process author - StringBuffer authorBuffer = new StringBuffer(); - for (; contributors.hasNext(); ) { - Contributor c = contributors.next(); - authorBuffer.append(getAuthorDetails(c)); - } - - if (authorBuffer.length() == 0) { - authorBuffer.append("Not defined"); - } - - return authorBuffer.toString(); - } - - /** - * Build a string that describes the specified author. - * - * @param author The author. - * - * @return The string description. - */ - private String getAuthorDetails(Author author) { - // process author - StringBuffer authorBuffer = new StringBuffer(); - authorBuffer.append(author.getName()); - authorBuffer.append(" (email: '"); - authorBuffer.append(author.getEmail()); - authorBuffer.append("', uri: '"); - authorBuffer.append(author.getUri()); - authorBuffer.append("')
"); - - return authorBuffer.toString(); - } - - /** - * Process the deposit response and insert the details into the tree. If the url - * matches one of the collections in the tree, the deposit is added as a child - * node. Otherwise, the node is added as a child of the root. - * - * @param url The url of the collection that the file was posted to. - * - * @param response The details of the deposit. - */ - public void processDepositResponse(String url, - DepositResponse response) { - SWORDEntry entry = response.getEntry(); - Object title = entry.getTitle(); - if (title == null) { - title = "Undefined"; - } else { - title = entry.getTitle().getContent(); - } - - TreeNodeWrapper wrapper = new TreeNodeWrapper(title.toString(), entry); - DefaultMutableTreeNode entryNode = new DefaultMutableTreeNode(wrapper); - - DefaultMutableTreeNode newParentNode = top; - List nodes = getCollectionNodes(); - for (DefaultMutableTreeNode node : nodes) { - Object o = node.getUserObject(); - if (o instanceof TreeNodeWrapper) { - TreeNodeWrapper collectionWrapper = (TreeNodeWrapper) o; - Object data = collectionWrapper.getData(); - if (data instanceof Collection) { - Collection col = (Collection) data; - String location = col.getLocation(); - if (location != null && location.equals(url)) { - newParentNode = node; - break; - } - } - } - } - - treeModel.insertNodeInto(entryNode, newParentNode, newParentNode.getChildCount()); - services.scrollPathToVisible(new TreePath(entryNode.getPath())); - } - - /** - * Get a list of all current collections displayed in the tree. - * - * @return An array of the URLs for the collections. - */ - public String[] getCollectionLocations() { - List nodes = getCollectionNodes(); - String[] locations = new String[nodes.size()]; - - DefaultMutableTreeNode node; - for (int i = 0; i < nodes.size(); i++) { - node = nodes.get(i); - Object o = node.getUserObject(); - if (o instanceof TreeNodeWrapper) { - TreeNodeWrapper collectionWrapper = (TreeNodeWrapper) o; - Object data = collectionWrapper.getData(); - if (data instanceof Collection) { - Collection col = (Collection) data; - String location = col.getLocation(); - if (location != null) { - locations[i] = location; - } - } - } - } - return locations; - } - - /** - * Get a list of nodes that contain collections. - * - * @return A vector of the collection nodes. - */ - private List getCollectionNodes() { - List nodes = new ArrayList(); - - DefaultMutableTreeNode node; - Enumeration treeNodes = top.depthFirstEnumeration(); - - while (treeNodes.hasMoreElements()) { - node = (DefaultMutableTreeNode) treeNodes.nextElement(); - Object o = node.getUserObject(); - if (o instanceof TreeNodeWrapper) { - TreeNodeWrapper wrapper = (TreeNodeWrapper) o; - Object data = wrapper.getData(); - if (data instanceof Collection) { - nodes.add(node); - } - } - } - - return nodes; - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ServiceSelectedListener.java b/dspace-sword/src/main/java/org/purl/sword/client/ServiceSelectedListener.java deleted file mode 100644 index fa97549fd924..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/ServiceSelectedListener.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * 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.purl.sword.client; - -/** - * Listener for any objects that want to be notified when a collection has been selected in the - * ServicePanel. - * - * @author Neil Taylor - */ -public interface ServiceSelectedListener { - /** - * Called to provide an update on whether the selected node is a Collection. - * - * @param collection The location of the collection. null, otherwise. - */ - public void selected(String collection); -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/ServletClient.java b/dspace-sword/src/main/java/org/purl/sword/client/ServletClient.java deleted file mode 100644 index 963e9637dd72..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/ServletClient.java +++ /dev/null @@ -1,455 +0,0 @@ -/** - * 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/ - */ - -/** - * Copyright (c) 2008, Aberystwyth University - * All rights reserved. - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * - Redistributions of source code must retain the above - * copyright notice, this list of conditions and the - * following disclaimer. - * - * - Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in - * the documentation and/or other materials provided with the - * distribution. - * - * - Neither the name of the Centre for Advanced Software and - * Intelligent Systems (CASIS) nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR - * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF - * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ - -package org.purl.sword.client; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.util.Iterator; -import java.util.List; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileItemFactory; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; -import org.purl.sword.base.DepositResponse; -import org.purl.sword.base.SWORDEntry; -import org.purl.sword.base.ServiceDocument; - -/** - * Example client that runs as a Servlet. - * - * @author Stuart Lewis - */ -public class ServletClient extends HttpServlet { - - /** - * The user agent name of this library - */ - public static final String userAgent = "SWORDAPP Java Client: SWORD version 1.3 compatible (http://sourceforge" + - ".net/projects/sword-app/)"; - - /** - * Temporary directory. - */ - private String tempDirectory; - - /** - * List of urls for the destination services to access. - */ - private String[] urls; - - /** - * Used to determine if a proxy value should be set. - */ - private boolean useProxy = false; - - /** - * The proxy host name. - */ - private String pHost; - - /** - * The proxy port name. - */ - private int pPort; - - /** Counter used during Deposit information. */ - private static int counter = 0; - - /** - * Initialise the servlet. - */ - public void init() { - tempDirectory = getServletContext().getInitParameter( - "upload-temp-directory"); - if ((tempDirectory == null) || (tempDirectory.equals(""))) { - tempDirectory = System.getProperty("java.io.tmpdir"); - } - String lots = getServletContext().getInitParameter("client-urls"); - urls = lots.split(","); - - pHost = getServletContext().getInitParameter("proxy-host"); - String pPortstr = getServletContext().getInitParameter("proxy-port"); - if (((pHost != null) && (!pHost.equals(""))) - && ((pPortstr != null) && (!pPortstr.equals("")))) { - try { - pPort = Integer.parseInt(pPortstr); - useProxy = true; - } catch (Exception e) { - // Port number not numeric - } - } - } - - /** - * Handle a get request. Simply show the default form (form.jsp) - * - * @param request The request details - * @param response The response to write to. - * - * @throws ServletException - * An exception that provides information on a database access error or other errors. - * @throws IOException - * A general class of exceptions produced by failed or interrupted I/O operations. - */ - protected void doGet(HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException { - // Get request, so show the default page - request.setAttribute("urls", urls); - request.getRequestDispatcher("form.jsp").forward(request, response); - } - - /** - * Process the post. Determine if the request is for a post or service - * document. Then, dispatch the request to the appropriate handler. - * - * @param request The request details. - * @param response The response to write to. - * - * @throws ServletException - * An exception that provides information on a database access error or other errors. - * @throws IOException - * A general class of exceptions produced by failed or interrupted I/O operations. - */ - - protected void doPost(HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException { - if (request.getParameter("servicedocument") != null) { - this.doServiceDocument(request, response); - } else if (request.getParameter("deposit") != null) { - request.setAttribute("url", request.getParameter("url")); - request.setAttribute("u", request.getParameter("u")); - request.setAttribute("p", request.getParameter("p")); - request.setAttribute("obo", request.getParameter("obo")); - request.setAttribute("abstract", request.getParameter("abstract")); - request.setAttribute("policy", request.getParameter("policy")); - request.setAttribute("treatment", request.getParameter("treatment")); - request.setAttribute("mediation", request.getParameter("mediation")); - request.setAttribute("accepts", request.getParameter("accepts")); - request.setAttribute("acceptsp", request.getParameter("acceptsp")); - request.setAttribute("maxuploadsize", request.getParameter("maxuploadsize")); - request.getRequestDispatcher("depositform.jsp").forward(request, response); - } else if (ServletFileUpload.isMultipartContent(request)) { - this.doDeposit(request, response); - } else { - request.setAttribute("urls", urls); - request.getRequestDispatcher("form.jsp").forward(request, response); - } - } - - /** - * Process the request for a service document. - * - * @param request The request details. - * @param response The response to write to. - * - * @throws ServletException - * An exception that provides information on a database access error or other errors. - * @throws IOException - * A general class of exceptions produced by failed or interrupted I/O operations. - */ - - private void doServiceDocument(HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException { - // Get the service document - Client client = new Client(); - // Which URL do we want? - URL url = new URL(request.getParameter("url")); - String theUrl = request.getParameter("url"); - - if ((request.getParameter("ownurl") != null) - && (!request.getParameter("ownurl").equals(""))) { - url = new URL(request.getParameter("ownurl")); - theUrl = request.getParameter("ownurl"); - } - - int port = url.getPort(); - if (port == -1) { - port = 80; - } - - // Set up the server - client.setServer(url.getHost(), port); - client.setCredentials(request.getParameter("u"), request.getParameter("p")); - if (useProxy) { - client.setProxy(pHost, pPort); - } - - try { - ServiceDocument sd = client.getServiceDocument(theUrl, - request.getParameter("obo")); - - // Set the status - Status status = client.getStatus(); - request.setAttribute("status", status.toString()); - if (status.getCode() == 200) { - // Set the debug response - String xml = sd.marshall(); - - String validateXml = xml; - validateXml = validateXml.replaceAll("&", "&"); - validateXml = validateXml.replaceAll("<", "<"); - validateXml = validateXml.replaceAll(">", ">"); - validateXml = validateXml.replaceAll("\"", """); - validateXml = validateXml.replaceAll("'", "'"); - request.setAttribute("xmlValidate", validateXml); // for passing to validation - - xml = xml.replaceAll("<", "<"); - xml = xml.replaceAll(">", ">"); - request.setAttribute("xml", xml); - - // Set the ServiceDocument and associated values - request.setAttribute("sd", sd); - request.setAttribute("sdURL", theUrl); - request.setAttribute("u", request.getParameter("u")); - request.setAttribute("p", request.getParameter("p")); - request.setAttribute("sdOBO", request.getParameter("obo")); - request.getRequestDispatcher("servicedocument.jsp").forward( - request, response); - return; - } else { - request.setAttribute("error", status.getCode() + " " - + status.getMessage()); - request.setAttribute("urls", urls); - request.getRequestDispatcher("form.jsp").forward(request, - response); - return; - } - } catch (SWORDClientException e) { - e.printStackTrace(); - request.setAttribute("error", e.toString()); - request.setAttribute("urls", urls); - request.getRequestDispatcher("form.jsp").forward(request, response); - } - } - - /** - * Process a deposit. - * - * @param request The request details. - * @param response The response to output to. - * - * @throws ServletException - * An exception that provides information on a database access error or other errors. - * @throws IOException - * A general class of exceptions produced by failed or interrupted I/O operations. - */ - private void doDeposit(HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException { - // Do the deposit - Client client = new Client(); - try { - PostMessage message = new PostMessage(); - message.setUserAgent(ClientConstants.SERVICE_NAME); - - // Get the file - FileItemFactory factory = new DiskFileItemFactory(); - - // Create a new file upload handler - ServletFileUpload upload = new ServletFileUpload(factory); - - // Parse the request - List items = upload.parseRequest(request); - Iterator iter = items.iterator(); - String u = null; - String p = null; - String contentDisposition = null; - String filetype = null; - boolean useMD5 = false; - boolean errorMD5 = false; - boolean verbose = false; - boolean noOp = false; - boolean login = false; - while (iter.hasNext()) { - FileItem item = iter.next(); - if (item.isFormField()) { - String name = item.getFieldName(); - String value = item.getString(); - if (name.equals("url")) { - message.setDestination(value); - URL url = new URL(value); - int port = url.getPort(); - if (port == -1) { - port = 80; - } - client.setServer(url.getHost(), port); - } else if (name.equals("usemd5")) { - useMD5 = true; - } else if (name.equals("errormd5")) { - errorMD5 = true; - } else if (name.equals("verbose")) { - verbose = true; - } else if (name.equals("noop")) { - noOp = true; - } else if (name.equals("obo")) { - message.setOnBehalfOf(value); - } else if (name.equals("slug")) { - if ((value != null) && (!value.trim().equals(""))) { - message.setSlug(value); - } - } else if (name.equals("cd")) { - contentDisposition = value; - } else if (name.equals("filetype")) { - filetype = value; - } else if (name.equals("formatnamespace")) { - if ((value != null) && (!value.trim().equals(""))) { - message.setFormatNamespace(value); - } - } else if (name.equals("u")) { - u = value; - login = true; - request.setAttribute("u", value); - } else if (name.equals("p")) { - p = value; - login = true; - } - request.setAttribute(name, value); - } else { - String fname = tempDirectory + File.separator + - "ServletClient-" + counter++; - if ((contentDisposition != null) && (!contentDisposition.equals(""))) { - fname = tempDirectory + File.separator + contentDisposition; - } - - File uploadedFile = new File(fname); - item.write(uploadedFile); - message.setFilepath(fname); - - if ((filetype == null) || (filetype.trim().equals(""))) { - message.setFiletype(item.getContentType()); - } else { - message.setFiletype(filetype); - } - } - } - - if (login) { - client.setCredentials(u, p); - } - - if (useProxy) { - client.setProxy(pHost, pPort); - } - - message.setUseMD5(useMD5); - message.setChecksumError(errorMD5); - message.setVerbose(verbose); - message.setNoOp(noOp); - - // Post the file - DepositResponse resp = client.postFile(message); - - // Set the status - Status status = client.getStatus(); - request.setAttribute("status", status.toString()); - if ((status.getCode() == 201) || (status.getCode() == 202)) { - // Set the debug response - String xml = resp.marshall(); - - String validateXml = xml; - validateXml = validateXml.replaceAll("&", "&"); - validateXml = validateXml.replaceAll("<", "<"); - validateXml = validateXml.replaceAll(">", ">"); - validateXml = validateXml.replaceAll("\"", """); - validateXml = validateXml.replaceAll("'", "'"); - request.setAttribute("xmlValidate", validateXml); // for passing to validation - - xml = xml.replaceAll("<", "<"); - xml = xml.replaceAll(">", ">"); - request.setAttribute("xml", xml); - SWORDEntry se = resp.getEntry(); - request.setAttribute("id", se.getId()); - request.setAttribute("authors", se.getAuthors()); - request.setAttribute("contributors", se.getContributors()); - request.setAttribute("title", se.getTitle().getContent()); - request.setAttribute("updated", se.getUpdated()); - request.setAttribute("categories", se.getCategories()); - request.setAttribute("treatment", se.getTreatment()); - request.setAttribute("summary", se.getSummary().getContent()); - request.setAttribute("generator", se.getGenerator().getContent()); - request.setAttribute("userAgent", se.getUserAgent()); - request.setAttribute("packaging", se.getPackaging()); - request.setAttribute("links", se.getLinks()); - request.setAttribute("location", resp.getLocation()); - - // Set the ServiceDocument and associated values - request.getRequestDispatcher("deposit.jsp").forward(request, response); - return; - } else { - String error = status.getCode() + " " + status.getMessage() + " - "; - try { - error += resp.getEntry().getSummary().getContent(); - } catch (Exception e) { - // Do nothing - we have default error message - e.printStackTrace(); - } - request.setAttribute("error", error); - - // Try and get an error document in xml - String xml = resp.marshall(); - xml = xml.replaceAll("<", "<"); - xml = xml.replaceAll(">", ">"); - request.setAttribute("xml", xml); - - request.getRequestDispatcher("depositform.jsp").forward(request, response); - return; - } - } catch (RuntimeException e) { - e.printStackTrace(); - request.setAttribute("error", "value: " + e.toString()); - request.setAttribute("urls", urls); - request.getRequestDispatcher("depositform.jsp").forward(request, response); - } catch (Exception e) { - e.printStackTrace(); - request.setAttribute("error", "value: " + e.toString()); - request.setAttribute("urls", urls); - request.getRequestDispatcher("depositform.jsp").forward(request, response); - } - } -} diff --git a/dspace-sword/src/main/java/org/purl/sword/client/Status.java b/dspace-sword/src/main/java/org/purl/sword/client/Status.java deleted file mode 100644 index ff80fa6e52e2..000000000000 --- a/dspace-sword/src/main/java/org/purl/sword/client/Status.java +++ /dev/null @@ -1,61 +0,0 @@ -/** - * 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.purl.sword.client; - -/** - * Representation of the status code and message. - * - * @author Neil Taylor - */ -public class Status { - /** - * The status code. - */ - private int code; - - /** - * The status message. - */ - private String message; - - /** - * Create a new status message. - * - * @param code The code. - * @param message The message. - */ - public Status(int code, String message) { - this.code = code; - this.message = message; - } - - /** - * Retrieve the code. - * - * @return The code. - */ - public int getCode() { - return code; - } - - /** - * Get the message. - * - * @return The message. - */ - public String getMessage() { - return message; - } - - /** - * Get a string representation of the status. - */ - public String toString() { - return "Code: " + code + ", Message: '" + message + "'"; - } -} From 68cffba5fa0557b4e3d1165373e4c35b00b33d1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 23:48:45 +0000 Subject: [PATCH 037/701] Bump the test-tools group with 6 updates Bumps the test-tools group with 6 updates: | Package | From | To | | --- | --- | --- | | [io.netty:netty-buffer](https://github.com/netty/netty) | `4.1.118.Final` | `4.1.119.Final` | | [io.netty:netty-transport](https://github.com/netty/netty) | `4.1.118.Final` | `4.1.119.Final` | | [io.netty:netty-transport-native-unix-common](https://github.com/netty/netty) | `4.1.118.Final` | `4.1.119.Final` | | [io.netty:netty-common](https://github.com/netty/netty) | `4.1.118.Final` | `4.1.119.Final` | | [io.netty:netty-handler](https://github.com/netty/netty) | `4.1.118.Final` | `4.1.119.Final` | | [io.netty:netty-codec](https://github.com/netty/netty) | `4.1.118.Final` | `4.1.119.Final` | Updates `io.netty:netty-buffer` from 4.1.118.Final to 4.1.119.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.118.Final...netty-4.1.119.Final) Updates `io.netty:netty-transport` from 4.1.118.Final to 4.1.119.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.118.Final...netty-4.1.119.Final) Updates `io.netty:netty-transport-native-unix-common` from 4.1.118.Final to 4.1.119.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.118.Final...netty-4.1.119.Final) Updates `io.netty:netty-common` from 4.1.118.Final to 4.1.119.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.118.Final...netty-4.1.119.Final) Updates `io.netty:netty-handler` from 4.1.118.Final to 4.1.119.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.118.Final...netty-4.1.119.Final) Updates `io.netty:netty-codec` from 4.1.118.Final to 4.1.119.Final - [Commits](https://github.com/netty/netty/compare/netty-4.1.118.Final...netty-4.1.119.Final) --- updated-dependencies: - dependency-name: io.netty:netty-buffer dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport-native-unix-common dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-common dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-handler dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-codec dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 322e1820d325..dc5ce96df3ca 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -887,32 +887,32 @@ io.netty netty-buffer - 4.1.118.Final + 4.2.0.Final io.netty netty-transport - 4.1.118.Final + 4.2.0.Final io.netty netty-transport-native-unix-common - 4.1.118.Final + 4.2.0.Final io.netty netty-common - 4.1.118.Final + 4.2.0.Final io.netty netty-handler - 4.1.118.Final + 4.2.0.Final io.netty netty-codec - 4.1.118.Final + 4.2.0.Final org.apache.velocity From c8cc4253578affcd984dd66591598957363d356b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:23:43 +0000 Subject: [PATCH 038/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.781 to 1.12.782 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.781 to 1.12.782. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.781...1.12.782) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 322e1820d325..50e5f44bc77a 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -761,7 +761,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.781 + 1.12.782 From b64b79973d2f6e7ce5a570eb0d8da932b62fe23a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:35:47 +0000 Subject: [PATCH 039/701] Bump the fasterxml group with 3 updates Bumps the fasterxml group with 3 updates: [com.fasterxml.jackson.core:jackson-annotations](https://github.com/FasterXML/jackson), [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) and [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson). Updates `com.fasterxml.jackson.core:jackson-annotations` from 2.18.2 to 2.18.3 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.core:jackson-core` from 2.18.2 to 2.18.3 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.2...jackson-core-2.18.3) Updates `com.fasterxml.jackson.core:jackson-core` from 2.18.2 to 2.18.3 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.2...jackson-core-2.18.3) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.18.2 to 2.18.3 - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-annotations dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 886aa3fb36cb..ec07adf6bf58 100644 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 3.10.8 2.31.0 - 2.18.2 - 2.18.2 + 2.18.3 + 2.18.3 1.3.2 2.3.1 2.3.9 From a5806fb518031e99d82a4af0bb625271f4830860 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:35:52 +0000 Subject: [PATCH 040/701] Bump the google-apis group across 1 directory with 4 updates Bumps the google-apis group with 4 updates in the / directory: [com.google.http-client:google-http-client](https://github.com/googleapis/google-http-java-client), [com.google.http-client:google-http-client-jackson2](https://github.com/googleapis/google-http-java-client), [com.google.oauth-client:google-oauth-client](https://github.com/googleapis/google-oauth-java-client) and [com.google.http-client:google-http-client-gson](https://github.com/googleapis/google-http-java-client). Updates `com.google.http-client:google-http-client` from 1.46.1 to 1.46.3 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.46.1...v1.46.3) Updates `com.google.http-client:google-http-client-jackson2` from 1.46.1 to 1.46.3 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.46.1...v1.46.3) Updates `com.google.oauth-client:google-oauth-client` from 1.37.0 to 1.38.0 - [Release notes](https://github.com/googleapis/google-oauth-java-client/releases) - [Changelog](https://github.com/googleapis/google-oauth-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-oauth-java-client/compare/v1.37.0...v1.38.0) Updates `com.google.http-client:google-http-client-gson` from 1.46.1 to 1.46.3 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.46.1...v1.46.3) --- updated-dependencies: - dependency-name: com.google.http-client:google-http-client dependency-type: direct:production update-type: version-update:semver-patch dependency-group: google-apis - dependency-name: com.google.http-client:google-http-client-jackson2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: google-apis - dependency-name: com.google.oauth-client:google-oauth-client dependency-type: direct:production update-type: version-update:semver-minor dependency-group: google-apis - dependency-name: com.google.http-client:google-http-client-gson dependency-type: direct:production update-type: version-update:semver-patch dependency-group: google-apis ... Signed-off-by: dependabot[bot] --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 886aa3fb36cb..6debf4a73f5c 100644 --- a/pom.xml +++ b/pom.xml @@ -1714,7 +1714,7 @@ com.google.http-client google-http-client - 1.46.1 + 1.46.3 com.google.errorprone @@ -1736,7 +1736,7 @@ com.google.http-client google-http-client-jackson2 - 1.46.1 + 1.46.3 jackson-core @@ -1751,14 +1751,14 @@ com.google.oauth-client google-oauth-client - 1.37.0 + 1.39.0 com.google.http-client google-http-client-gson - 1.46.1 + 1.46.3 From bb2ed04ed3f42ff86f51fe00c9805f646a009049 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:36:26 +0000 Subject: [PATCH 041/701] Bump the build-tools group across 1 directory with 5 updates Bumps the build-tools group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [com.github.spotbugs:spotbugs](https://github.com/spotbugs/spotbugs) | `4.9.1` | `4.9.3` | | [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) | `3.13.0` | `3.14.0` | | [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) | `4.9.1.0` | `4.9.3.0` | | [org.apache.maven.plugins:maven-clean-plugin](https://github.com/apache/maven-clean-plugin) | `3.4.0` | `3.4.1` | | [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) | `0.8.12` | `0.8.13` | Updates `com.github.spotbugs:spotbugs` from 4.9.1 to 4.9.3 - [Release notes](https://github.com/spotbugs/spotbugs/releases) - [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotbugs/spotbugs/compare/4.9.1...4.9.3) Updates `org.apache.maven.plugins:maven-compiler-plugin` from 3.13.0 to 3.14.0 - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.13.0...maven-compiler-plugin-3.14.0) Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.9.1.0 to 4.9.3.0 - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.1.0...spotbugs-maven-plugin-4.9.3.0) Updates `org.apache.maven.plugins:maven-clean-plugin` from 3.4.0 to 3.4.1 - [Release notes](https://github.com/apache/maven-clean-plugin/releases) - [Commits](https://github.com/apache/maven-clean-plugin/compare/maven-clean-plugin-3.4.0...maven-clean-plugin-3.4.1) Updates `org.jacoco:jacoco-maven-plugin` from 0.8.12 to 0.8.13 - [Release notes](https://github.com/jacoco/jacoco/releases) - [Commits](https://github.com/jacoco/jacoco/compare/v0.8.12...v0.8.13) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs dependency-version: 4.9.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.3.0 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-clean-plugin dependency-version: 3.4.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.jacoco:jacoco-maven-plugin dependency-version: 0.8.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 886aa3fb36cb..3e8092aa5cac 100644 --- a/pom.xml +++ b/pom.xml @@ -140,7 +140,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.14.0 11 @@ -295,7 +295,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.1.0 + 4.9.3.0 Max Low @@ -305,7 +305,7 @@ com.github.spotbugs spotbugs - 4.9.1 + 4.9.3 @@ -321,7 +321,7 @@ maven-clean-plugin - 3.4.0 + 3.4.1 @@ -392,7 +392,7 @@ org.jacoco jacoco-maven-plugin - 0.8.12 + 0.8.13 From 16239433f62233244d73f7e92715d7b6d2768694 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:47:34 +0000 Subject: [PATCH 042/701] Bump io.grpc:grpc-context from 1.70.0 to 1.71.0 Bumps [io.grpc:grpc-context](https://github.com/grpc/grpc-java) from 1.70.0 to 1.71.0. - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.70.0...v1.71.0) --- updated-dependencies: - dependency-name: io.grpc:grpc-context dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 886aa3fb36cb..6bc32b120619 100644 --- a/pom.xml +++ b/pom.xml @@ -1731,7 +1731,7 @@ io.grpc grpc-context - 1.70.0 + 1.71.0 com.google.http-client From e122a90674e1e36222fd62f46538020d77d2a88f Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Tue, 11 Mar 2025 10:58:08 +0100 Subject: [PATCH 043/701] Implement a SEOHealthIndicator which verifies all relevant parameters for SEO are ok (cherry picked from commit 4bd8a24ca75f6d2e6384e850b45c96c4f1229f02) --- .../configuration/ActuatorConfiguration.java | 7 ++ .../app/rest/health/SEOHealthIndicator.java | 77 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java index ad78fe2db40b..15b9bd9506f9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/configuration/ActuatorConfiguration.java @@ -14,6 +14,7 @@ import org.apache.solr.client.solrj.SolrServerException; import org.dspace.app.rest.DiscoverableEndpointsService; import org.dspace.app.rest.health.GeoIpHealthIndicator; +import org.dspace.app.rest.health.SEOHealthIndicator; import org.dspace.authority.AuthoritySolrServiceImpl; import org.dspace.discovery.SolrSearchCore; import org.dspace.statistics.SolrStatisticsCore; @@ -82,6 +83,12 @@ public SolrHealthIndicator solrOaiCoreHealthIndicator(SolrServerResolver solrSer return new SolrHealthIndicator(solrServerResolver.getServer()); } + @Bean + @ConditionalOnEnabledHealthIndicator("seo") + public SEOHealthIndicator seoHealthIndicator() { + return new SEOHealthIndicator(); + } + @Bean @ConditionalOnEnabledHealthIndicator("geoIp") public GeoIpHealthIndicator geoIpHealthIndicator() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java new file mode 100644 index 000000000000..d936fce635e6 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java @@ -0,0 +1,77 @@ +/** + * 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.app.rest.health; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.springframework.boot.actuate.health.AbstractHealthIndicator; +import org.springframework.boot.actuate.health.Health; +import org.springframework.web.client.RestTemplate; + +/** + * Implementation of {@link org.springframework.boot.actuate.health.HealthIndicator} that verifies if the SEO of the + * DSpace instance is configured correctly. + * + * This is only relevant in a production environment, where the DSpace instance is exposed to the public. + */ +public class SEOHealthIndicator extends AbstractHealthIndicator { + + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + protected void doHealthCheck(Health.Builder builder) { + String baseUrl = configurationService.getProperty("dspace.ui.url"); + + boolean sitemapOk = checkUrl(baseUrl + "/sitemap_index.xml") || checkUrl(baseUrl + "/sitemap_index.html"); + boolean robotsTxtOk = checkRobotsTxt(baseUrl + "/robots.txt"); + boolean ssrOk = checkSSR(baseUrl); + + if (sitemapOk && robotsTxtOk && ssrOk) { + builder.up() + .withDetail("sitemap", "OK") + .withDetail("robots.txt", "OK") + .withDetail("ssr", "OK"); + } else { + builder.down() + .withDetail("sitemap", sitemapOk ? "OK" : "Missing or inaccessible") + .withDetail("robots.txt", robotsTxtOk ? "OK" : "Empty or contains local URLs") + .withDetail("ssr", ssrOk ? "OK" : "Server-side rendering might be disabled"); + } + } + + private boolean checkUrl(String url) { + try { + restTemplate.getForEntity(url, String.class); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean checkRobotsTxt(String url) { + try { + String content = restTemplate.getForObject(url, String.class); + return StringUtils.isNotBlank(content) && !content.contains("localhost"); + } catch (Exception e) { + return false; + } + } + + private boolean checkSSR(String url) { + try { + String content = restTemplate.getForObject(url, String.class); + return content != null && !content.contains(""); + } catch (Exception e) { + return false; + } + } +} + From 31549bdaceb9220272d3645100fc8b48f28c7fa2 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Tue, 18 Mar 2025 10:04:36 +0100 Subject: [PATCH 044/701] Disable new actuator in IT (cherry picked from commit 20ab43ccccf84c83d6db9b431321e60256d30355) --- dspace-api/src/test/data/dspaceFolder/config/local.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 0b4fe8288cfa..ab0d14de6ee3 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -158,6 +158,7 @@ proxies.trusted.include_ui_ip = true # For the tests we have to disable this health indicator because there isn't a mock server and the calculated status was DOWN management.health.solrOai.enabled = false +management.health.seo.enabled = false # Enable researcher profiles and orcid synchronization for tests researcher-profile.entity-type = Person From 7a876999f8af137ced2064f5c9d3b7360399e6c0 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Tue, 1 Apr 2025 17:54:48 +0200 Subject: [PATCH 045/701] 127746: Implement different failures for robots file so we can differentiate between a missing file or an invalid file (cherry picked from commit 32c048428026f890c2f3bc7eca5a2f20717dc587) --- .../app/rest/health/SEOHealthIndicator.java | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java index d936fce635e6..a071e12088bd 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java @@ -31,19 +31,28 @@ protected void doHealthCheck(Health.Builder builder) { String baseUrl = configurationService.getProperty("dspace.ui.url"); boolean sitemapOk = checkUrl(baseUrl + "/sitemap_index.xml") || checkUrl(baseUrl + "/sitemap_index.html"); - boolean robotsTxtOk = checkRobotsTxt(baseUrl + "/robots.txt"); + RobotsTxtStatus robotsTxtStatus = checkRobotsTxt(baseUrl + "/robots.txt"); boolean ssrOk = checkSSR(baseUrl); - if (sitemapOk && robotsTxtOk && ssrOk) { + if (sitemapOk && robotsTxtStatus == RobotsTxtStatus.VALID && ssrOk) { builder.up() .withDetail("sitemap", "OK") .withDetail("robots.txt", "OK") .withDetail("ssr", "OK"); } else { - builder.down() - .withDetail("sitemap", sitemapOk ? "OK" : "Missing or inaccessible") - .withDetail("robots.txt", robotsTxtOk ? "OK" : "Empty or contains local URLs") - .withDetail("ssr", ssrOk ? "OK" : "Server-side rendering might be disabled"); + builder.down(); + builder.withDetail("sitemap", sitemapOk ? "OK" : "Missing or inaccessible"); + + if (robotsTxtStatus == RobotsTxtStatus.MISSING) { + builder.withDetail("robots.txt", "Missing or inaccessible. Please see the DSpace Documentation on " + + "Search Engine Optimization for how to create a robots.txt."); + } else if (robotsTxtStatus == RobotsTxtStatus.INVALID) { + builder.withDetail("robots.txt", "Invalid because it contains localhost URLs. This is often a sign " + + "that a proxy is failing to pass X-Forwarded headers to DSpace. Please see the DSpace " + + "Documentation on Search Engine Optimization for how to pass X-Forwarded headers."); + } + + builder.withDetail("ssr", ssrOk ? "OK" : "Server-side rendering might be disabled"); } } @@ -56,12 +65,18 @@ private boolean checkUrl(String url) { } } - private boolean checkRobotsTxt(String url) { + private RobotsTxtStatus checkRobotsTxt(String url) { try { String content = restTemplate.getForObject(url, String.class); - return StringUtils.isNotBlank(content) && !content.contains("localhost"); + if (StringUtils.isBlank(content)) { + return RobotsTxtStatus.MISSING; + } + if (content.contains("localhost")) { + return RobotsTxtStatus.INVALID; + } + return RobotsTxtStatus.VALID; } catch (Exception e) { - return false; + return RobotsTxtStatus.MISSING; } } @@ -73,5 +88,9 @@ private boolean checkSSR(String url) { return false; } } + + private enum RobotsTxtStatus { + VALID, MISSING, INVALID + } } From 64c5f822091133e95e668d6d05a11887ef572153 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Tue, 1 Apr 2025 17:58:47 +0200 Subject: [PATCH 046/701] 127746: Add more detailed information messages on how to solve problems (cherry picked from commit 170dc9a44c5b16c28298a9e3133534e70967147d) --- .../org/dspace/app/rest/health/SEOHealthIndicator.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java index a071e12088bd..740c6ab6493e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java @@ -41,7 +41,8 @@ protected void doHealthCheck(Health.Builder builder) { .withDetail("ssr", "OK"); } else { builder.down(); - builder.withDetail("sitemap", sitemapOk ? "OK" : "Missing or inaccessible"); + builder.withDetail("sitemap", sitemapOk ? "OK" : "Sitemaps are missing or inaccessible. Please see the " + + "DSpace Documentation on Search Engine Optimization for how to enable Sitemaps."); if (robotsTxtStatus == RobotsTxtStatus.MISSING) { builder.withDetail("robots.txt", "Missing or inaccessible. Please see the DSpace Documentation on " + @@ -51,8 +52,9 @@ protected void doHealthCheck(Health.Builder builder) { "that a proxy is failing to pass X-Forwarded headers to DSpace. Please see the DSpace " + "Documentation on Search Engine Optimization for how to pass X-Forwarded headers."); } - - builder.withDetail("ssr", ssrOk ? "OK" : "Server-side rendering might be disabled"); + builder.withDetail("ssr", ssrOk ? "OK" : "Server-side rendering (SSR) appears to be disabled. Most " + + "search engines require enabling SSR for proper indexing. Please see the DSpace Documentation on" + + " Search Engine Optimization for more details."); } } From 5b8782509fd857214934dca457f304b77e2c43d8 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Fri, 4 Apr 2025 16:51:56 +0200 Subject: [PATCH 047/701] 127746: Include success result for robots.txt check if other checks fail (cherry picked from commit 5dc12775fac0006dbc1d0106ffcdffbe893919d1) --- .../java/org/dspace/app/rest/health/SEOHealthIndicator.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java index 740c6ab6493e..5b57f2d537fc 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/health/SEOHealthIndicator.java @@ -51,6 +51,8 @@ protected void doHealthCheck(Health.Builder builder) { builder.withDetail("robots.txt", "Invalid because it contains localhost URLs. This is often a sign " + "that a proxy is failing to pass X-Forwarded headers to DSpace. Please see the DSpace " + "Documentation on Search Engine Optimization for how to pass X-Forwarded headers."); + } else { + builder.withDetail("robots.txt", "OK"); } builder.withDetail("ssr", ssrOk ? "OK" : "Server-side rendering (SSR) appears to be disabled. Most " + "search engines require enabling SSR for proper indexing. Please see the DSpace Documentation on" + From dd62a57564a72e82f4c702c985927a7159dc912d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:11:31 +0000 Subject: [PATCH 048/701] Bump the apache-commons group with 2 updates Bumps the apache-commons group with 2 updates: commons-io:commons-io and org.apache.commons:commons-text. Updates `commons-io:commons-io` from 2.18.0 to 2.19.0 Updates `org.apache.commons:commons-text` from 1.13.0 to 1.13.1 --- updated-dependencies: - dependency-name: commons-io:commons-io dependency-version: 2.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-text dependency-version: 1.13.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d48979c1c0c8..b5d4fdfb66d0 100644 --- a/pom.xml +++ b/pom.xml @@ -1505,7 +1505,7 @@ commons-io commons-io - 2.18.0 + 2.19.0 org.apache.commons @@ -1532,7 +1532,7 @@ org.apache.commons commons-text - 1.13.0 + 1.13.1 commons-validator From 086e54cb31cfac31814ca642ef99a9b541520d51 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Sun, 27 Oct 2024 09:40:05 +0100 Subject: [PATCH 049/701] Modify Solr query to find collections with submit permissions in searches with spaces (cherry picked from commit 425dc1556ed27d20ff57e0b043f7bf2d021c3096) --- .../dspace/content/CollectionServiceImpl.java | 3 ++- .../app/rest/CollectionRestRepositoryIT.java | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index 5fe8ca54d9c7..1b051ae32ffb 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -1021,7 +1021,8 @@ private DiscoverResult retrieveCollectionsWithSubmit(Context context, DiscoverQu if (StringUtils.isNotBlank(q)) { StringBuilder buildQuery = new StringBuilder(); String escapedQuery = ClientUtils.escapeQueryChars(q); - buildQuery.append("(").append(escapedQuery).append(" OR ").append(escapedQuery).append("*").append(")"); + buildQuery.append("(").append(escapedQuery).append(" OR dc.title_sort:*") + .append(escapedQuery).append("*").append(")"); discoverQuery.setQuery(buildQuery.toString()); } DiscoverResult resp = searchService.search(context, discoverQuery); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java index 50e60087adf0..1e18723ca90c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java @@ -698,6 +698,10 @@ public void findAuthorizedCollectionsWithQueryTest() throws Exception { .withName("Testing autocomplete in submission") .withSubmitterGroup(eperson2) .build(); + Collection col5 = CollectionBuilder.createCollection(context, child2) + .withName("Colección de prueba") + .withSubmitterGroup(eperson2) + .build(); context.restoreAuthSystemState(); String tokenEPerson = getAuthToken(eperson.getEmail(), password); @@ -741,7 +745,19 @@ public void findAuthorizedCollectionsWithQueryTest() throws Exception { getClient(tokenEPerson2).perform(get("/api/core/collections/search/findSubmitAuthorized") .param("query", "testing auto")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", is(0))); + .andExpect(jsonPath("$._embedded.collections", Matchers.contains( + CollectionMatcher.matchProperties(col4.getName(), col4.getID(), col4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Diacritics test + getClient(tokenEPerson2).perform(get("/api/core/collections/search/findSubmitAuthorized") + .param("query", "coléccion de")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains( + CollectionMatcher.matchProperties(col5.getName(), col5.getID(), col5.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); String tokenAdmin = getAuthToken(admin.getEmail(), password); getClient(tokenAdmin).perform(get("/api/core/collections/search/findSubmitAuthorized") From 3a894e5f5d4146ce120fecad3a72f3632dada7d0 Mon Sep 17 00:00:00 2001 From: Piaget Bouaka Donfack Date: Wed, 16 Apr 2025 17:11:09 +0200 Subject: [PATCH 050/701] [DURACOM-346] SubscribeServiceImpl : the method "isSubscribed" returns incorrect result (cherry picked from commit 23468d4ee357363dddd0996a1a13366be88527c8) --- .../src/main/java/org/dspace/eperson/SubscribeServiceImpl.java | 3 ++- .../src/test/java/org/dspace/eperson/SubscribeServiceIT.java | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java index 2e4d94f4431e..0f5d2ba319cd 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java @@ -131,7 +131,8 @@ public List findAvailableSubscriptions(Context context, EPerson eper @Override public boolean isSubscribed(Context context, EPerson eperson, DSpaceObject dSpaceObject) throws SQLException { - return subscriptionDAO.findByEPersonAndDso(context, eperson, dSpaceObject, -1, -1) != null; + List subscriptions = subscriptionDAO.findByEPersonAndDso(context, eperson, dSpaceObject, -1, -1); + return subscriptions != null && !subscriptions.isEmpty(); } @Override diff --git a/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java b/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java index 945dd481d00a..128b2e552b88 100644 --- a/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java @@ -213,6 +213,7 @@ public void subscribingUserUnsubscribesTheirSubscription() throws Exception { secondCollection, 100, 0); assertEquals(subscriptions.size(), 1); + assertThat(subscribeService.isSubscribed(context, subscribingUser, secondCollection), is(true)); subscribeService.unsubscribe(context, subscribingUser, secondCollection); @@ -222,6 +223,7 @@ public void subscribingUserUnsubscribesTheirSubscription() throws Exception { secondCollection, 100, 0); assertEquals(subscriptions.size(), 0); + assertThat(subscribeService.isSubscribed(context, subscribingUser, secondCollection), is(false)); } @Test(expected = AuthorizeException.class) From d6ff41d9f5d8295e95f5ed40dd7f0ab8caf7c3a9 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Tue, 18 Mar 2025 23:11:51 +0100 Subject: [PATCH 051/701] Refactor browse entries facet query to use JSON facet query (cherry picked from commit 8e88547932c9c0260c50307d7c90657182de45af) --- .../java/org/dspace/browse/SolrBrowseDAO.java | 67 +++++++------------ .../org/dspace/discovery/SolrServiceImpl.java | 56 +++++++++------- 2 files changed, 56 insertions(+), 67 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java index 1917dec423ec..14dab3e56174 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -23,7 +23,6 @@ import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.core.Context; -import org.dspace.discovery.DiscoverFacetField; import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.DiscoverQuery.SORT_ORDER; import org.dspace.discovery.DiscoverResult; @@ -34,7 +33,6 @@ import org.dspace.discovery.SearchServiceException; import org.dspace.discovery.SearchUtils; import org.dspace.discovery.configuration.DiscoveryConfiguration; -import org.dspace.discovery.configuration.DiscoveryConfigurationParameters; import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.services.factory.DSpaceServicesFactory; @@ -181,32 +179,28 @@ private DiscoverResult getSolrResponse() throws BrowseException { addLocationScopeFilter(query); addDefaultFilterQueries(query); if (distinct) { - DiscoverFacetField dff; - - // To get the number of distinct values we use the next "json.facet" query param - // {"entries_count": {"type":"terms","field": "_filter", "limit":0, "numBuckets":true}}" + // We use a json.facet query for metadata browsing because it allows us to limit the results + // while obtaining the total number of facet values with numBuckets:true and sort in reverse order + // Example of json.facet query: + // {"": {"type":"terms","field": "_filter", "limit":0, "offset":0, + // "sort":"index desc", "numBuckets":true, "prefix":""}} ObjectNode jsonFacet = JsonNodeFactory.instance.objectNode(); - ObjectNode entriesCount = JsonNodeFactory.instance.objectNode(); - entriesCount.put("type", "terms"); - entriesCount.put("field", facetField + "_filter"); - entriesCount.put("limit", 0); - entriesCount.put("numBuckets", true); - jsonFacet.set("entries_count", entriesCount); - + ObjectNode entriesFacet = JsonNodeFactory.instance.objectNode(); + entriesFacet.put("type", "terms"); + entriesFacet.put("field", facetField + "_filter"); + entriesFacet.put("limit", limit); + entriesFacet.put("offset", offset); + entriesFacet.put("numBuckets", true); + if (ascending) { + entriesFacet.put("sort", "index"); + } else { + entriesFacet.put("sort", "index desc"); + } if (StringUtils.isNotBlank(startsWith)) { - dff = new DiscoverFacetField(facetField, - DiscoveryConfigurationParameters.TYPE_TEXT, limit, - DiscoveryConfigurationParameters.SORT.VALUE, startsWith, offset); - // Add the prefix to the json facet query - entriesCount.put("prefix", startsWith); - } else { - dff = new DiscoverFacetField(facetField, - DiscoveryConfigurationParameters.TYPE_TEXT, limit, - DiscoveryConfigurationParameters.SORT.VALUE, offset); + entriesFacet.put("prefix", startsWith); } - query.addFacetField(dff); - query.setFacetMinCount(1); + jsonFacet.set(facetField, entriesFacet); query.setMaxResults(0); query.addProperty("json.facet", jsonFacet.toString()); } else { @@ -282,26 +276,15 @@ public List doValueQuery() throws BrowseException { DiscoverResult resp = getSolrResponse(); List facet = resp.getFacetResult(facetField); int count = doCountQuery(); - int start = 0; int max = facet.size(); List result = new ArrayList<>(); - if (ascending) { - for (int i = start; i < (start + max) && i < count; i++) { - FacetResult c = facet.get(i); - String freq = showFrequencies ? String.valueOf(c.getCount()) - : ""; - result.add(new String[] {c.getDisplayedValue(), - c.getAuthorityKey(), freq}); - } - } else { - for (int i = count - start - 1; i >= count - (start + max) - && i >= 0; i--) { - FacetResult c = facet.get(i); - String freq = showFrequencies ? String.valueOf(c.getCount()) - : ""; - result.add(new String[] {c.getDisplayedValue(), - c.getAuthorityKey(), freq}); - } + + for (int i = 0; i < max && i < count; i++) { + FacetResult c = facet.get(i); + String freq = showFrequencies ? String.valueOf(c.getCount()) + : ""; + result.add(new String[] {c.getDisplayedValue(), + c.getAuthorityKey(), freq}); } return result; diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index 29b695b5e785..e4889cb8cba8 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -42,6 +42,7 @@ import org.apache.solr.client.solrj.response.FacetField; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.client.solrj.response.json.BucketBasedJsonFacet; +import org.apache.solr.client.solrj.response.json.BucketJsonFacet; import org.apache.solr.client.solrj.response.json.NestableJsonFacet; import org.apache.solr.client.solrj.util.ClientUtils; import org.apache.solr.common.SolrDocument; @@ -1057,8 +1058,8 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query) } //Resolve our facet field values resolveFacetFields(context, query, result, skipLoadingResponse, solrQueryResponse); - //Add total entries count for metadata browsing - resolveEntriesCount(result, solrQueryResponse); + //Resolve our json facet field values used for metadata browsing + resolveJsonFacetFields(context, result, solrQueryResponse); } // If any stale entries are found in the current page of results, // we remove those stale entries and rerun the same query again. @@ -1085,33 +1086,38 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query) } /** - * Stores the total count of entries for metadata index browsing. The count is calculated by the - * json.facet parameter with the following value: + * Process the 'json.facet' response, which is currently only used for metadata browsing * - *

-     * {
-     *     "entries_count": {
-     *         "type": "terms",
-     *         "field": "facetNameField_filter",
-     *         "limit": 0,
-     *         "prefix": "prefix_value",
-     *         "numBuckets": true
-     *     }
-     * }
-     * 
- * - * This value is returned in the facets field of the Solr response. - * - * @param result DiscoverResult object where the total entries count will be stored - * @param solrQueryResponse QueryResponse object containing the solr response + * @param context context object + * @param result the result object to add the facet results to + * @param solrQueryResponse the solr query response + * @throws SQLException if database error */ - private void resolveEntriesCount(DiscoverResult result, QueryResponse solrQueryResponse) { + private void resolveJsonFacetFields(Context context, DiscoverResult result, QueryResponse solrQueryResponse) + throws SQLException { NestableJsonFacet response = solrQueryResponse.getJsonFacetingResponse(); - if (response != null) { - BucketBasedJsonFacet facet = response.getBucketBasedFacets("entries_count"); - if (facet != null) { - result.setTotalEntries(facet.getNumBucketsCount()); + if (response != null && response.getBucketBasedFacetNames() != null) { + for (String facetName : response.getBucketBasedFacetNames()) { + BucketBasedJsonFacet facet = response.getBucketBasedFacets(facetName); + if (facet != null) { + result.setTotalEntries(facet.getNumBucketsCount()); + for (BucketJsonFacet bucket : facet.getBuckets()) { + String facetValue = bucket.getVal() != null ? bucket.getVal().toString() : ""; + String field = facetName + "_filter"; + String displayedValue = transformDisplayedValue(context, field, facetValue); + String authorityValue = transformAuthorityValue(context, field, facetValue); + String sortValue = transformSortValue(context, field, facetValue); + String filterValue = displayedValue; + if (StringUtils.isNotBlank(authorityValue)) { + filterValue = authorityValue; + } + result.addFacetResult(facetName, + new DiscoverResult.FacetResult(filterValue, displayedValue, + authorityValue, sortValue, bucket.getCount(), + DiscoveryConfigurationParameters.TYPE_TEXT)); + } + } } } } From b2f44f57f9c75fc6594243207de9eed168dce6f3 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Sun, 23 Mar 2025 23:53:58 +0100 Subject: [PATCH 052/701] Add test for browse entries pagination (cherry picked from commit a7bc82084ecf97ec9706c0ea77c24faa39b946fb) --- .../app/rest/BrowsesResourceControllerIT.java | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java index d1791ab872bb..6b8113e5afac 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java @@ -259,6 +259,185 @@ public void findBrowseBySubjectEntries() throws Exception { ))); } + @Test + public void findBrowseBySubjectEntriesPagination() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build(); + + //2. Three public items that are readable by Anonymous with different subjects + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("ExtraEntry") + .build(); + + Item publicItem2 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 2") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("TestingForMore").withSubject("ExtraEntry") + .build(); + + Item publicItem3 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 2") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry") + .build(); + Item withdrawnItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Withdrawn item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("WithdrawnEntry") + .withdrawn() + .build(); + Item privateItem1 = ItemBuilder.createItem(context, col2) + .withTitle("Private item 1") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry").withSubject("PrivateEntry") + .makeUnDiscoverable() + .build(); + + + + context.restoreAuthSystemState(); + + //** WHEN ** + //An anonymous user browses this endpoint to find which subjects are currently in the repository + getClient().perform(get("/api/discover/browses/subject/entries") + .param("projection", "full") + .param("size", "1")) + + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.number", is(0))) + //Check that there are indeed 3 different subjects + .andExpect(jsonPath("$.page.totalElements", is(3))) + //Check that the subject matches as expected + .andExpect(jsonPath("$._embedded.entries", + contains(BrowseEntryResourceMatcher.matchBrowseEntry("AnotherTest", 1) + ))); + + getClient().perform(get("/api/discover/browses/subject/entries") + .param("projection", "full") + .param("size", "1") + .param("page","1")) + + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.number", is(1))) + //Check that there are indeed 3 different subjects + .andExpect(jsonPath("$.page.totalElements", is(3))) + //Check that the subject matches as expected + .andExpect(jsonPath("$._embedded.entries", + contains(BrowseEntryResourceMatcher.matchBrowseEntry("ExtraEntry", 3) + ))); + + getClient().perform(get("/api/discover/browses/subject/entries") + .param("projection", "full") + .param("size", "1") + .param("page","2")) + + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.number", is(2))) + //Check that there are indeed 3 different subjects + .andExpect(jsonPath("$.page.totalElements", is(3))) + //Check that the subject matches as expected + .andExpect(jsonPath("$._embedded.entries", + contains(BrowseEntryResourceMatcher.matchBrowseEntry("TestingForMore", 2) + ))); + + getClient().perform(get("/api/discover/browses/subject/entries") + .param("sort", "value,desc") + .param("size", "1")) + + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.number", is(0))) + //Check that there are indeed 3 different subjects + .andExpect(jsonPath("$.page.totalElements", is(3))) + //Check that the subject matches as expected + .andExpect(jsonPath("$._embedded.entries", + contains(BrowseEntryResourceMatcher.matchBrowseEntry("TestingForMore", 2) + ))); + + getClient().perform(get("/api/discover/browses/subject/entries") + .param("sort", "value,desc") + .param("size", "1") + .param("page","1")) + + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.number", is(1))) + //Check that there are indeed 3 different subjects + .andExpect(jsonPath("$.page.totalElements", is(3))) + //Check that the subject matches as expected + .andExpect(jsonPath("$._embedded.entries", + contains(BrowseEntryResourceMatcher.matchBrowseEntry("ExtraEntry", 3) + ))); + + getClient().perform(get("/api/discover/browses/subject/entries") + .param("sort", "value,desc") + .param("size", "1") + .param("page","2")) + + //** THEN ** + //The status has to be 200 + .andExpect(status().isOk()) + + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.number", is(2))) + //Check that there are indeed 3 different subjects + .andExpect(jsonPath("$.page.totalElements", is(3))) + //Check that the subject matches as expected + .andExpect(jsonPath("$._embedded.entries", + contains(BrowseEntryResourceMatcher.matchBrowseEntry("AnotherTest", 1) + ))); + } + @Test public void findBrowseBySubjectEntriesWithAuthority() throws Exception { configurationService.setProperty("choices.plugin.dc.subject", From 0a79903f3009569aada1a5ceac6d0857e466ba56 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Thu, 20 Feb 2025 18:21:21 +0100 Subject: [PATCH 053/701] 126885: Removed database connection leak on login Also: - Updated EPersonRestAuthenticationProvider to not open an additional DB connection, and reuse the existing one instead - Normalized the behaviour of OidcLoginFilter by not calling the redirectAfterSuccess instead of doing a chain.doFilter(req, res). This way we don't need to reopen a new Context (cherry picked from commit 518fb3b1d89795b4995b32e164e3ac9c9e48350e) --- .../EPersonRestAuthenticationProvider.java | 64 ++++++++----------- .../app/rest/security/OidcLoginFilter.java | 52 ++++++++++++++- ...JWTTokenRestAuthenticationServiceImpl.java | 3 + 3 files changed, 80 insertions(+), 39 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java index e55734e513de..00c804ad2d64 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java @@ -85,7 +85,7 @@ public Authentication authenticate(Authentication authentication) throws Authent } else { // Otherwise, this is a new login & we need to attempt authentication log.debug("Request to authenticate new login"); - return authenticateNewLogin(authentication); + return authenticateNewLogin(context, authentication); } } @@ -107,56 +107,44 @@ private Authentication authenticateRefreshTokenRequest(Context context) { * If login is successful, returns a NEW Authentication class containing the logged in EPerson and their list of * GrantedAuthority objects. If login fails, a BadCredentialsException is thrown. If no valid login found implicit * or explicit, then null is returned. + * + * @param context The current DSpace context * @param authentication Authentication class to attempt authentication. * @return new Authentication class containing logged-in user information or null */ - private Authentication authenticateNewLogin(Authentication authentication) { - Context newContext = null; + private Authentication authenticateNewLogin(Context context, Authentication authentication) { Authentication output = null; if (authentication != null) { - try { - newContext = new Context(); - String name = authentication.getName(); - String password = Objects.toString(authentication.getCredentials(), null); + String name = authentication.getName(); + String password = Objects.toString(authentication.getCredentials(), null); - int implicitStatus = authenticationService.authenticateImplicit(newContext, null, null, null, request); + int implicitStatus = authenticationService.authenticateImplicit(context, null, null, null, request); - if (implicitStatus == AuthenticationMethod.SUCCESS) { - log.info(LogHelper.getHeader(newContext, "login", "type=implicit")); - output = createAuthentication(newContext); - } else { - int authenticateResult = authenticationService - .authenticate(newContext, name, password, null, request); - if (AuthenticationMethod.SUCCESS == authenticateResult) { + if (implicitStatus == AuthenticationMethod.SUCCESS) { + log.info(LogHelper.getHeader(context, "login", "type=implicit")); + output = createAuthentication(context); + } else { + int authenticateResult = authenticationService.authenticate(context, name, password, null, request); + if (AuthenticationMethod.SUCCESS == authenticateResult) { - log.info(LogHelper - .getHeader(newContext, "login", "type=explicit")); + log.info(LogHelper.getHeader(context, "login", "type=explicit")); - output = createAuthentication(newContext); + output = createAuthentication(context); - for (PostLoggedInAction action : postLoggedInActions) { - try { - action.loggedIn(newContext); - } catch (Exception ex) { - log.error("An error occurs performing post logged in action", ex); - } + for (PostLoggedInAction action : postLoggedInActions) { + try { + action.loggedIn(context); + } catch (Exception ex) { + log.error("An error occurs performing post logged in action", ex); } - - } else { - log.info(LogHelper.getHeader(newContext, "failed_login", "email=" - + name + ", result=" - + authenticateResult)); - throw new BadCredentialsException("Login failed"); - } - } - } finally { - if (newContext != null && newContext.isValid()) { - try { - newContext.complete(); - } catch (SQLException e) { - log.error(e.getMessage() + " occurred while trying to close", e); } + + } else { + log.info(LogHelper.getHeader(context, "failed_login", "email=" + + name + ", result=" + + authenticateResult)); + throw new BadCredentialsException("Login failed"); } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OidcLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OidcLoginFilter.java index c84840e77041..e73a3e8d3ccb 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OidcLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OidcLoginFilter.java @@ -10,11 +10,18 @@ import static org.dspace.authenticate.OidcAuthenticationBean.OIDC_AUTH_ATTRIBUTE; import java.io.IOException; +import java.util.ArrayList; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.core.Utils; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -27,6 +34,11 @@ public class OidcLoginFilter extends StatelessLoginFilter { + private static final Logger log = LogManager.getLogger(OidcLoginFilter.class); + + private final ConfigurationService configurationService = DSpaceServicesFactory.getInstance() + .getConfigurationService(); + public OidcLoginFilter(String url, AuthenticationManager authenticationManager, RestAuthenticationService restAuthenticationService) { super(url, authenticationManager, restAuthenticationService); @@ -44,7 +56,45 @@ public Authentication attemptAuthentication(HttpServletRequest req, HttpServletR protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { restAuthenticationService.addAuthenticationDataForUser(req, res, (DSpaceAuthentication) auth, true); - chain.doFilter(req, res); + redirectAfterSuccess(req, res); + } + + /** + * After successful login, redirect to the DSpace URL specified by this OIDC + * request (in the "redirectUrl" request parameter). If that 'redirectUrl' is + * not valid or trusted for this DSpace site, then return a 400 error. + * @param request + * @param response + * @throws IOException + */ + private void redirectAfterSuccess(HttpServletRequest request, HttpServletResponse response) throws IOException { + // Get redirect URL from request parameter + String redirectUrl = request.getParameter("redirectUrl"); + + // If redirectUrl unspecified, default to the configured UI + if (StringUtils.isEmpty(redirectUrl)) { + redirectUrl = configurationService.getProperty("dspace.ui.url"); + } + + // Validate that the redirectURL matches either the server or UI hostname. It + // *cannot* be an arbitrary URL. + String redirectHostName = Utils.getHostName(redirectUrl); + String serverHostName = Utils.getHostName(configurationService.getProperty("dspace.server.url")); + ArrayList allowedHostNames = new ArrayList<>(); + allowedHostNames.add(serverHostName); + String[] allowedUrls = configurationService.getArrayProperty("rest.cors.allowed-origins"); + for (String url : allowedUrls) { + allowedHostNames.add(Utils.getHostName(url)); + } + + if (StringUtils.equalsAnyIgnoreCase(redirectHostName, allowedHostNames.toArray(new String[0]))) { + log.debug("OIDC redirecting to " + redirectUrl); + response.sendRedirect(redirectUrl); + } else { + log.error("Invalid OIDC redirectURL=" + redirectUrl + ". URL doesn't match hostname of server or UI!"); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, + "Invalid redirectURL! Must match server or ui hostname."); + } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index c28729ff83a8..425ba505b431 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -84,6 +84,9 @@ public void addAuthenticationDataForUser(HttpServletRequest request, HttpServlet String token = loginJWTTokenHandler.createTokenForEPerson(context, request, authentication.getPreviousLoginDate()); context.commit(); + // Close the Context, because the DSpaceRequestContextFilter is not called for requests that trigger + // the authentication filters (filters that extend AbstractAuthenticationProcessingFilter) + context.close(); // Add newly generated auth token to the response addTokenToResponse(request, response, token, addCookie); From 133c2808836713e58bdfb5ed98ddecd2aefb43ab Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Thu, 17 Apr 2025 00:57:54 +0200 Subject: [PATCH 054/701] 126885: Removed database connection leak on logout (cherry picked from commit b299a960763136b7b2b973575e9dd25901a365eb) --- .../java/org/dspace/app/rest/security/CustomLogoutHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java index b3f4a00d379e..1f7331f5f0f1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java @@ -45,7 +45,7 @@ public void logout(HttpServletRequest httpServletRequest, HttpServletResponse ht try { Context context = ContextUtil.obtainContext(httpServletRequest); restAuthenticationService.invalidateAuthenticationData(httpServletRequest, httpServletResponse, context); - context.commit(); + context.complete(); } catch (Exception e) { log.error("Unable to logout", e); From 5d880bcf2da5391700ae9bc5fec6d84dc3314bfe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 23:03:07 +0000 Subject: [PATCH 055/701] Bump io.grpc:grpc-context from 1.71.0 to 1.72.0 Bumps [io.grpc:grpc-context](https://github.com/grpc/grpc-java) from 1.71.0 to 1.72.0. - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.71.0...v1.72.0) --- updated-dependencies: - dependency-name: io.grpc:grpc-context dependency-version: 1.72.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b5d4fdfb66d0..aff93df4a23c 100644 --- a/pom.xml +++ b/pom.xml @@ -1731,7 +1731,7 @@ io.grpc grpc-context - 1.71.0 + 1.72.0 com.google.http-client From 440bb648090ebe7992faf406c491b9168bb91009 Mon Sep 17 00:00:00 2001 From: Agustina Martinez Date: Thu, 20 Mar 2025 12:56:11 +0000 Subject: [PATCH 056/701] Update dim.xsl Added template to correctly parse elements under "others" metadata element (cherry picked from commit ac7da6a477cb7bb42b41560a751223efc870dafa) --- .../config/crosswalks/oai/metadataFormats/dim.xsl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dspace/config/crosswalks/oai/metadataFormats/dim.xsl b/dspace/config/crosswalks/oai/metadataFormats/dim.xsl index ea0aad182092..b659fef931a8 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/dim.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/dim.xsl @@ -36,6 +36,18 @@ + + + + + + + + + + + + @@ -62,6 +74,7 @@ + From 045a5c0b0eff8bc52f52d452e98fc9b22a8e45d4 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 1 Apr 2025 19:49:28 +0200 Subject: [PATCH 057/701] add method getMaxNumOfItemsPerRequest --- .../java/org/dspace/app/util/service/OpenSearchService.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/app/util/service/OpenSearchService.java b/dspace-api/src/main/java/org/dspace/app/util/service/OpenSearchService.java index 03f41e535c53..08900f8fff9c 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/service/OpenSearchService.java +++ b/dspace-api/src/main/java/org/dspace/app/util/service/OpenSearchService.java @@ -117,4 +117,10 @@ public Document getResultsDoc(Context context, String format, String query, int public DSpaceObject resolveScope(Context context, String scope) throws SQLException; + /** + * Retrieves the maximum number of items that can be included in a single opensearch request. + * + * @return the maximum number of items allowed per request + */ + int getMaxNumOfItemsPerRequest(); } From 869d122eaca9d052693bb13fe2faf061b6836026 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 1 Apr 2025 19:50:48 +0200 Subject: [PATCH 058/701] implement method getMaxNumOfItemsPerRequest --- .../java/org/dspace/app/util/OpenSearchServiceImpl.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java index bff741b5ca42..2075ef7a3816 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/OpenSearchServiceImpl.java @@ -101,6 +101,14 @@ protected String getBaseSearchUIURL() { configurationService.getProperty("websvc.opensearch.uicontext"); } + /** + * Get base search UI URL (websvc.opensearch.max_num_of_items_per_request) + */ + public int getMaxNumOfItemsPerRequest() { + return configurationService.getIntProperty( + "websvc.opensearch.max_num_of_items_per_request", 100); + } + @Override public String getContentType(String format) { return "html".equals(format) ? "text/html" : From ca3b2de1a83a3ee654c054cb70666d7c15f8bee4 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 1 Apr 2025 19:51:28 +0200 Subject: [PATCH 059/701] add configuration key websvc.opensearch.max_num_of_items_per_request --- dspace/config/dspace.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 91057ef1ec68..94b731cd3488 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1362,7 +1362,8 @@ websvc.opensearch.tags = IR DSpace # result formats offered - use 1 or more comma-separated from: html,atom,rss # html uses the normal search module websvc.opensearch.formats = html,atom,rss - +# maximum number of item per request +websvc.opensearch.max_num_of_items_per_request = 100 #### Content Inline Disposition Threshold #### # From 1a619b28336030e01211497cd8e466d4f3beb92c Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 1 Apr 2025 19:59:48 +0200 Subject: [PATCH 060/701] restrict maximum value of URL parameter rpp --- .../dspace/app/rest/OpenSearchController.java | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java index fef6269e3eee..d4e0036449a4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OpenSearchController.java @@ -21,17 +21,13 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.ScopeResolver; import org.dspace.app.util.SyndicationFeed; import org.dspace.app.util.factory.UtilServiceFactory; import org.dspace.app.util.service.OpenSearchService; -import org.dspace.authorize.factory.AuthorizeServiceFactory; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.content.factory.ContentServiceFactory; -import org.dspace.content.service.CollectionService; -import org.dspace.content.service.CommunityService; import org.dspace.core.Context; import org.dspace.core.LogHelper; import org.dspace.core.Utils; @@ -50,7 +46,6 @@ import org.dspace.discovery.indexobject.IndexableItem; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -67,12 +62,9 @@ public class OpenSearchController { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); - private static final String errorpath = "/error"; + private List searchIndices = null; - private CommunityService communityService; - private CollectionService collectionService; - private AuthorizeService authorizeService; private OpenSearchService openSearchService; @Autowired @@ -99,22 +91,28 @@ public void search(HttpServletRequest request, @RequestParam(name = "format", required = false) String format, @RequestParam(name = "sort", required = false) String sort, @RequestParam(name = "sort_direction", required = false) String sortDirection, - @RequestParam(name = "scope", required = false) String dsoObject, - Model model) throws IOException, ServletException { + @RequestParam(name = "scope", required = false) String dsoObject) + throws IOException, ServletException { context = ContextUtil.obtainContext(request); - if (start == null) { - start = 0; - } - if (count == null) { - count = -1; - } + if (openSearchService == null) { openSearchService = UtilServiceFactory.getInstance().getOpenSearchService(); } + if (openSearchService.isEnabled()) { init(); + + if (start == null) { + start = 0; + } + + if (count == null) { + count = -1; + } + count = Math.min(count, openSearchService.getMaxNumOfItemsPerRequest()); + // get enough request parameters to decide on action to take - if (format == null || "".equals(format)) { + if (StringUtils.isEmpty(format)) { // default to atom format = "atom"; } @@ -266,9 +264,6 @@ private void init() { searchIndices.add(sFilter.getIndexFieldName()); } } - communityService = ContentServiceFactory.getInstance().getCommunityService(); - collectionService = ContentServiceFactory.getInstance().getCollectionService(); - authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); } public void setOpenSearchService(OpenSearchService oSS) { From ac7dfc562fd90d161c7fa0f99cd54b00b16f64b3 Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Tue, 22 Apr 2025 22:12:00 -0300 Subject: [PATCH 061/701] fix: import public email from ORCID person (cherry picked from commit 82ca80cd6b5d340f5d3d2e042803c2fc43d8a973) --- .../impl/OrcidV3AuthorDataProvider.java | 22 +- .../impl/OrcidV3AuthorDataProviderTest.java | 231 ++++++++++++++++++ .../provider/impl/orcid-person/person1.xml | 51 ++++ .../provider/impl/orcid-person/person2.xml | 64 +++++ .../provider/impl/orcid-person/person3.xml | 35 +++ .../provider/impl/orcid-person/search.xml | 25 ++ .../provider/orcid-v3-author/person1.xml | 51 ++++ .../provider/orcid-v3-author/person2.xml | 64 +++++ .../provider/orcid-v3-author/person3.xml | 35 +++ .../provider/orcid-v3-author/search.xml | 25 ++ 10 files changed, 596 insertions(+), 7 deletions(-) create mode 100644 dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java create mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person1.xml create mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person2.xml create mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person3.xml create mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/search.xml create mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person1.xml create mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person2.xml create mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person3.xml create mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/search.xml diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index c7e41171a5bb..dfbd07a83a02 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -27,6 +27,7 @@ import org.dspace.external.provider.orcid.xml.XMLtoBio; import org.dspace.orcid.model.factory.OrcidFactoryUtils; import org.orcid.jaxb.model.v3.release.common.OrcidIdentifier; +import org.orcid.jaxb.model.v3.release.record.Email; import org.orcid.jaxb.model.v3.release.record.Person; import org.orcid.jaxb.model.v3.release.search.Result; import org.springframework.beans.factory.annotation.Autowired; @@ -114,13 +115,20 @@ protected ExternalDataObject convertToExternalDataObject(Person person) { if (person.getName().getFamilyName() != null) { lastName = person.getName().getFamilyName().getContent(); externalDataObject.addMetadata(new MetadataValueDTO("person", "familyName", null, null, - lastName)); + lastName)); } if (person.getName().getGivenNames() != null) { firstName = person.getName().getGivenNames().getContent(); externalDataObject.addMetadata(new MetadataValueDTO("person", "givenName", null, null, - firstName)); - + firstName)); + } + if (person.getEmails().getEmails() != null && !person.getEmails().getEmails().isEmpty()) { + Email email = person.getEmails().getEmails().get(0); + if (person.getEmails().getEmails().size() > 1) { + email = person.getEmails().getEmails().stream().filter(Email::isPrimary).findFirst().orElse(email); + } + externalDataObject.addMetadata(new MetadataValueDTO("person", "email", null, + null, email.getEmail())); } externalDataObject.setId(person.getName().getPath()); externalDataObject @@ -128,7 +136,7 @@ protected ExternalDataObject convertToExternalDataObject(Person person) { new MetadataValueDTO("person", "identifier", "orcid", null, person.getName().getPath())); externalDataObject .addMetadata(new MetadataValueDTO("dc", "identifier", "uri", null, - orcidUrl + "/" + person.getName().getPath())); + orcidUrl + "/" + person.getName().getPath())); if (!StringUtils.isBlank(lastName) && !StringUtils.isBlank(firstName)) { externalDataObject.setDisplayValue(lastName + ", " + firstName); externalDataObject.setValue(lastName + ", " + firstName); @@ -139,8 +147,8 @@ protected ExternalDataObject convertToExternalDataObject(Person person) { externalDataObject.setDisplayValue(firstName); externalDataObject.setValue(firstName); } - } else if (person.getPath() != null ) { - externalDataObject.setId(StringUtils.substringBetween(person.getPath(),"/","/person")); + } else if (person.getPath() != null) { + externalDataObject.setId(StringUtils.substringBetween(person.getPath(), "/", "/person")); } return externalDataObject; } @@ -204,7 +212,7 @@ public List searchExternalDataObjects(String query, int star for (Result result : results) { OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); if (orcidIdentifier != null) { - log.debug("Found OrcidId=" + orcidIdentifier.toString()); + log.debug("Found OrcidId=" + orcidIdentifier.getPath()); String orcid = orcidIdentifier.getPath(); Person bio = getBio(orcid); if (bio != null) { diff --git a/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java b/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java new file mode 100644 index 000000000000..34b3a6838d4e --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java @@ -0,0 +1,231 @@ +/** + * 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.external.provider.impl; + +import org.dspace.AbstractDSpaceTest; +import org.dspace.external.OrcidRestConnector; +import org.dspace.external.model.ExternalDataObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.InputStream; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link OrcidV3AuthorDataProvider}. + * + * @author Jesiel Viana (jesielviana at proton.me) + * + */ +public class OrcidV3AuthorDataProviderTest extends AbstractDSpaceTest { + + private static final String SEARCH_XML_PATH = "org/dspace/external/provider/orcid-v3-author/search.xml"; + private static final String PERSON1_XML_PATH = "org/dspace/external/provider/orcid-v3-author/person1.xml"; + private static final String PERSON2_XML_PATH = "org/dspace/external/provider/orcid-v3-author/person2.xml"; + private static final String PERSON3_XML_PATH = "org/dspace/external/provider/orcid-v3-author/person3.xml"; + + public static final String ORCID_SEARCH_QUERY = "search?q=0000-0000-0000-0000"; + + private OrcidV3AuthorDataProvider dataProvider; + + @Before + public void setup() throws Exception { + dataProvider = new OrcidV3AuthorDataProvider(); + + OrcidRestConnector mockRestConnector = mock(OrcidRestConnector.class); + + dataProvider.setOrcidRestConnector(mockRestConnector); + dataProvider.setSourceIdentifier("orcid"); + dataProvider.setOrcidUrl("https://orcid.org"); + + dataProvider.setClientId("client-id"); + dataProvider.setClientSecret("client-secret"); + dataProvider.setOAUTHUrl("https://orcid.org/oauth"); + + InputStream searchXmlStream = getClass().getClassLoader().getResourceAsStream(SEARCH_XML_PATH); + InputStream person1XmlStream = getClass().getClassLoader().getResourceAsStream(PERSON1_XML_PATH); + InputStream person2XmlStream = getClass().getClassLoader().getResourceAsStream(PERSON2_XML_PATH); + InputStream person3XmlStream = getClass().getClassLoader().getResourceAsStream(PERSON3_XML_PATH); + + when(mockRestConnector.get("search?q=search%3Fq%3D0000-0000-0000-0000&start=0&rows=10",null )).thenReturn(searchXmlStream); + when(mockRestConnector.get("0000-0000-0000-0001/person",null )).thenReturn(person1XmlStream); + when(mockRestConnector.get("0000-0000-0000-0002/person",null )).thenReturn(person2XmlStream); + when(mockRestConnector.get("0000-0000-0000-0003/person",null )).thenReturn(person3XmlStream); + + } + + @Test + public void testGetExternalDataObjectSizeIsCorrect() { + List optional = dataProvider.searchExternalDataObjects(ORCID_SEARCH_QUERY, 0, 10); + assertThat(optional, hasSize(3)); + } + + @Test + public void testGetExternalDataObjectGetPersonWithAllFieldsPopulated() { + List optional = dataProvider.searchExternalDataObjects(ORCID_SEARCH_QUERY, 0, 10); + + assertThat(optional, hasSize(3)); + + ExternalDataObject externalDataObject1 = optional.get(0); + + // Basic field assertions + assertThat(externalDataObject1.getId(), equalTo("0000-0000-0000-0001")); + assertThat(externalDataObject1.getValue(), equalTo("FamilyName1, GivenNames1")); + assertThat(externalDataObject1.getSource(), equalTo("orcid")); + assertThat(externalDataObject1.getDisplayValue(), equalTo("FamilyName1, GivenNames1")); + + // Metadata assertions + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("familyName")), + hasProperty("value", equalTo("FamilyName1")) + ) + )); + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("givenName")), + hasProperty("value", equalTo("GivenNames1")) + ) + )); + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("email")), + hasProperty("value", equalTo("person1@email.com")) + ) + )); + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("orcid")), + hasProperty("value", equalTo("0000-0000-0000-0001")) + ) + )); + assertThat(externalDataObject1.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("dc")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("uri")), + hasProperty("value", equalTo("https://orcid.org/0000-0000-0000-0001")) + ) + )); + } + + @Test + public void testGetExternalDataObjectGetPrimaryEmailFromPersonWithTwoEmails() { + List optional = dataProvider.searchExternalDataObjects(ORCID_SEARCH_QUERY, 0, 10); + + assertThat(optional, hasSize(3)); + + ExternalDataObject externalDataObject2 = optional.get(1); // Test person2 (with two emails) + + // Basic field assertions + assertThat(externalDataObject2.getId(), equalTo("0000-0000-0000-0002")); + assertThat(externalDataObject2.getValue(), equalTo("FamilyName2, GivenNames2")); + assertThat(externalDataObject2.getSource(), equalTo("orcid")); + assertThat(externalDataObject2.getDisplayValue(), equalTo("FamilyName2, GivenNames2")); + + // Metadata assertions + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("familyName")), + hasProperty("value", equalTo("FamilyName2")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("givenName")), + hasProperty("value", equalTo("GivenNames2")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("email")), + hasProperty("value", equalTo("person2primary@email.com")) // Primary email + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("orcid")), + hasProperty("value", equalTo("0000-0000-0000-0002")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("dc")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("uri")), + hasProperty("value", equalTo("https://orcid.org/0000-0000-0000-0002")) + ) + )); + } + + + @Test + public void testGetExternalDataObjectGetPersonOnlyWithNameFilled() { + List optional = dataProvider.searchExternalDataObjects(ORCID_SEARCH_QUERY, 0, 10); + + assertThat(optional, hasSize(3)); + + ExternalDataObject externalDataObject2 = optional.get(2); // Test person2 (with two emails) + + // Basic field assertions + assertThat(externalDataObject2.getId(), equalTo("0000-0000-0000-0003")); + assertThat(externalDataObject2.getValue(), equalTo("FamilyName3, GivenNames3")); + assertThat(externalDataObject2.getSource(), equalTo("orcid")); + assertThat(externalDataObject2.getDisplayValue(), equalTo("FamilyName3, GivenNames3")); + + // Metadata assertions + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("familyName")), + hasProperty("value", equalTo("FamilyName3")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("givenName")), + hasProperty("value", equalTo("GivenNames3")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("person")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("orcid")), + hasProperty("value", equalTo("0000-0000-0000-0003")) + ) + )); + assertThat(externalDataObject2.getMetadata(), hasItem( + allOf( + hasProperty("schema", equalTo("dc")), + hasProperty("element", equalTo("identifier")), + hasProperty("qualifier", equalTo("uri")), + hasProperty("value", equalTo("https://orcid.org/0000-0000-0000-0003")) + ) + )); + } +} diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person1.xml b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person1.xml new file mode 100644 index 000000000000..64e4b292b92b --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person1.xml @@ -0,0 +1,51 @@ + + + 2025-04-21T22:28:18.862Z + + 2025-04-11T15:41:21.340Z + 2025-04-11T15:41:21.340Z + GivenNames1 + FamilyName1 + + + + + 2025-04-21T22:28:18.862Z + + 2025-04-21T22:23:14.698Z + 2025-04-21T22:28:18.862Z + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + GivenNames1 FamilyName1 + + person1@email.com + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person2.xml b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person2.xml new file mode 100644 index 000000000000..c91b0207247d --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person2.xml @@ -0,0 +1,64 @@ + + + 2025-04-21T22:28:18.862Z + + 2025-04-11T15:41:21.340Z + 2025-04-11T15:41:21.340Z + GivenNames2 + FamilyName2 + + + + + 2025-04-21T22:28:18.862Z + + 2025-04-21T22:23:14.698Z + 2025-04-21T22:28:18.862Z + + + https://sandbox.orcid.org/0000-0000-0000-0002 + 0000-0000-0000-0002 + sandbox.orcid.org + + GivenNames2 FamilyName2 + + person2@email.com + + + 2025-04-21T16:42:54.961Z + 2025-04-21T16:48:32.642Z + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + GivenNames1 FamilyName1 + + person2primary@email.com + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person3.xml b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person3.xml new file mode 100644 index 000000000000..b24ed9d3547a --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person3.xml @@ -0,0 +1,35 @@ + + + + 2024-06-11T20:01:28.538Z + 2024-06-11T20:01:28.538Z + GivenNames3 + FamilyName3 + + + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/search.xml b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/search.xml new file mode 100644 index 000000000000..98ec721be9b8 --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/search.xml @@ -0,0 +1,25 @@ + + + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + + + + https://sandbox.orcid.org/0000-0000-0000-0002 + 0000-0000-0000-0002 + sandbox.orcid.org + + + + + https://sandbox.orcid.org/0000-0000-0000-0003 + 0000-0000-0000-0003 + sandbox.orcid.org + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person1.xml b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person1.xml new file mode 100644 index 000000000000..64e4b292b92b --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person1.xml @@ -0,0 +1,51 @@ + + + 2025-04-21T22:28:18.862Z + + 2025-04-11T15:41:21.340Z + 2025-04-11T15:41:21.340Z + GivenNames1 + FamilyName1 + + + + + 2025-04-21T22:28:18.862Z + + 2025-04-21T22:23:14.698Z + 2025-04-21T22:28:18.862Z + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + GivenNames1 FamilyName1 + + person1@email.com + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person2.xml b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person2.xml new file mode 100644 index 000000000000..c91b0207247d --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person2.xml @@ -0,0 +1,64 @@ + + + 2025-04-21T22:28:18.862Z + + 2025-04-11T15:41:21.340Z + 2025-04-11T15:41:21.340Z + GivenNames2 + FamilyName2 + + + + + 2025-04-21T22:28:18.862Z + + 2025-04-21T22:23:14.698Z + 2025-04-21T22:28:18.862Z + + + https://sandbox.orcid.org/0000-0000-0000-0002 + 0000-0000-0000-0002 + sandbox.orcid.org + + GivenNames2 FamilyName2 + + person2@email.com + + + 2025-04-21T16:42:54.961Z + 2025-04-21T16:48:32.642Z + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + GivenNames1 FamilyName1 + + person2primary@email.com + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person3.xml b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person3.xml new file mode 100644 index 000000000000..b24ed9d3547a --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/person3.xml @@ -0,0 +1,35 @@ + + + + 2024-06-11T20:01:28.538Z + 2024-06-11T20:01:28.538Z + GivenNames3 + FamilyName3 + + + + + + + + diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/search.xml b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/search.xml new file mode 100644 index 000000000000..98ec721be9b8 --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/external/provider/orcid-v3-author/search.xml @@ -0,0 +1,25 @@ + + + + + https://sandbox.orcid.org/0000-0000-0000-0001 + 0000-0000-0000-0001 + sandbox.orcid.org + + + + + https://sandbox.orcid.org/0000-0000-0000-0002 + 0000-0000-0000-0002 + sandbox.orcid.org + + + + + https://sandbox.orcid.org/0000-0000-0000-0003 + 0000-0000-0000-0003 + sandbox.orcid.org + + + From cf9e5c1cd4c91f2b3d072c2a18eed3ca2441454a Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Tue, 22 Apr 2025 23:19:56 -0300 Subject: [PATCH 062/701] fix: Checkstyle violations (cherry picked from commit 9a831e53933771228c7e141db3d0217651d5b32c) --- .../impl/OrcidV3AuthorDataProviderTest.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java b/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java index 34b3a6838d4e..a68c0519bae5 100644 --- a/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProviderTest.java @@ -7,22 +7,24 @@ */ package org.dspace.external.provider.impl; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.beans.HasPropertyWithValue.hasProperty; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.AllOf.allOf; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.util.List; + import org.dspace.AbstractDSpaceTest; import org.dspace.external.OrcidRestConnector; import org.dspace.external.model.ExternalDataObject; -import org.junit.After; import org.junit.Before; import org.junit.Test; -import java.io.InputStream; -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; /** * Unit tests for {@link OrcidV3AuthorDataProvider}. @@ -60,10 +62,11 @@ public void setup() throws Exception { InputStream person2XmlStream = getClass().getClassLoader().getResourceAsStream(PERSON2_XML_PATH); InputStream person3XmlStream = getClass().getClassLoader().getResourceAsStream(PERSON3_XML_PATH); - when(mockRestConnector.get("search?q=search%3Fq%3D0000-0000-0000-0000&start=0&rows=10",null )).thenReturn(searchXmlStream); - when(mockRestConnector.get("0000-0000-0000-0001/person",null )).thenReturn(person1XmlStream); - when(mockRestConnector.get("0000-0000-0000-0002/person",null )).thenReturn(person2XmlStream); - when(mockRestConnector.get("0000-0000-0000-0003/person",null )).thenReturn(person3XmlStream); + when(mockRestConnector.get("search?q=search%3Fq%3D0000-0000-0000-0000&start=0&rows=10", null)) + .thenReturn(searchXmlStream); + when(mockRestConnector.get("0000-0000-0000-0001/person", null)).thenReturn(person1XmlStream); + when(mockRestConnector.get("0000-0000-0000-0002/person", null)).thenReturn(person2XmlStream); + when(mockRestConnector.get("0000-0000-0000-0003/person", null)).thenReturn(person3XmlStream); } From 106936967e6d2bcd861cefe9ecc05bbb47cb9a93 Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Tue, 22 Apr 2025 23:44:10 -0300 Subject: [PATCH 063/701] removing duplicated files (cherry picked from commit c6d1121cbe90ee9c12847f283a3d00601acceaa3) --- .../provider/impl/orcid-person/person1.xml | 51 --------------- .../provider/impl/orcid-person/person2.xml | 64 ------------------- .../provider/impl/orcid-person/person3.xml | 35 ---------- .../provider/impl/orcid-person/search.xml | 25 -------- 4 files changed, 175 deletions(-) delete mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person1.xml delete mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person2.xml delete mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person3.xml delete mode 100644 dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/search.xml diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person1.xml b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person1.xml deleted file mode 100644 index 64e4b292b92b..000000000000 --- a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person1.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - 2025-04-21T22:28:18.862Z - - 2025-04-11T15:41:21.340Z - 2025-04-11T15:41:21.340Z - GivenNames1 - FamilyName1 - - - - - 2025-04-21T22:28:18.862Z - - 2025-04-21T22:23:14.698Z - 2025-04-21T22:28:18.862Z - - - https://sandbox.orcid.org/0000-0000-0000-0001 - 0000-0000-0000-0001 - sandbox.orcid.org - - GivenNames1 FamilyName1 - - person1@email.com - - - - - - diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person2.xml b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person2.xml deleted file mode 100644 index c91b0207247d..000000000000 --- a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person2.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - 2025-04-21T22:28:18.862Z - - 2025-04-11T15:41:21.340Z - 2025-04-11T15:41:21.340Z - GivenNames2 - FamilyName2 - - - - - 2025-04-21T22:28:18.862Z - - 2025-04-21T22:23:14.698Z - 2025-04-21T22:28:18.862Z - - - https://sandbox.orcid.org/0000-0000-0000-0002 - 0000-0000-0000-0002 - sandbox.orcid.org - - GivenNames2 FamilyName2 - - person2@email.com - - - 2025-04-21T16:42:54.961Z - 2025-04-21T16:48:32.642Z - - - https://sandbox.orcid.org/0000-0000-0000-0001 - 0000-0000-0000-0001 - sandbox.orcid.org - - GivenNames1 FamilyName1 - - person2primary@email.com - - - - - - diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person3.xml b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person3.xml deleted file mode 100644 index b24ed9d3547a..000000000000 --- a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/person3.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - 2024-06-11T20:01:28.538Z - 2024-06-11T20:01:28.538Z - GivenNames3 - FamilyName3 - - - - - - - - diff --git a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/search.xml b/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/search.xml deleted file mode 100644 index 98ec721be9b8..000000000000 --- a/dspace-api/src/test/resources/org/dspace/external/provider/impl/orcid-person/search.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - https://sandbox.orcid.org/0000-0000-0000-0001 - 0000-0000-0000-0001 - sandbox.orcid.org - - - - - https://sandbox.orcid.org/0000-0000-0000-0002 - 0000-0000-0000-0002 - sandbox.orcid.org - - - - - https://sandbox.orcid.org/0000-0000-0000-0003 - 0000-0000-0000-0003 - sandbox.orcid.org - - - From b949bdd4c0c54a42edb343d65af1696e43ebbc4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 23:58:24 +0000 Subject: [PATCH 064/701] Bump the fasterxml group with 3 updates Bumps the fasterxml group with 3 updates: [com.fasterxml.jackson.core:jackson-annotations](https://github.com/FasterXML/jackson), [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) and [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson). Updates `com.fasterxml.jackson.core:jackson-annotations` from 2.18.3 to 2.19.0 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.core:jackson-core` from 2.18.3 to 2.19.0 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.3...jackson-core-2.19.0) Updates `com.fasterxml.jackson.core:jackson-core` from 2.18.3 to 2.19.0 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.3...jackson-core-2.19.0) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.18.3 to 2.19.0 - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-annotations dependency-version: 2.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index aff93df4a23c..39dddad33f66 100644 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 3.10.8 2.31.0 - 2.18.3 - 2.18.3 + 2.19.0 + 2.19.0 1.3.2 2.3.1 2.3.9 From ff67241bc9d336e488dad643d7a4af35ddb1518b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:01:34 +0000 Subject: [PATCH 065/701] Bump the apache-commons group with 2 updates Bumps the apache-commons group with 2 updates: org.apache.commons:commons-collections4 and org.apache.commons:commons-configuration2. Updates `org.apache.commons:commons-collections4` from 4.4 to 4.5.0 Updates `org.apache.commons:commons-configuration2` from 2.11.0 to 2.12.0 --- updated-dependencies: - dependency-name: org.apache.commons:commons-collections4 dependency-version: 4.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-configuration2 dependency-version: 2.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index aff93df4a23c..ac8dde9b55ad 100644 --- a/pom.xml +++ b/pom.xml @@ -1485,12 +1485,12 @@ org.apache.commons commons-collections4 - 4.4 + 4.5.0 org.apache.commons commons-configuration2 - 2.11.0 + 2.12.0 org.apache.commons From 5400e3f8a815bc12fa5e50a56a3626643a8b5bb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:02:22 +0000 Subject: [PATCH 066/701] Bump the google-apis group with 3 updates Bumps the google-apis group with 3 updates: [com.google.http-client:google-http-client](https://github.com/googleapis/google-http-java-client), [com.google.http-client:google-http-client-jackson2](https://github.com/googleapis/google-http-java-client) and [com.google.http-client:google-http-client-gson](https://github.com/googleapis/google-http-java-client). Updates `com.google.http-client:google-http-client` from 1.46.3 to 1.47.0 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.46.3...v1.47.0) Updates `com.google.http-client:google-http-client-jackson2` from 1.46.3 to 1.47.0 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.46.3...v1.47.0) Updates `com.google.http-client:google-http-client-gson` from 1.46.3 to 1.47.0 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.46.3...v1.47.0) --- updated-dependencies: - dependency-name: com.google.http-client:google-http-client dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: google-apis - dependency-name: com.google.http-client:google-http-client-jackson2 dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: google-apis - dependency-name: com.google.http-client:google-http-client-gson dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: google-apis ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index aff93df4a23c..d847bcc2811b 100644 --- a/pom.xml +++ b/pom.xml @@ -1714,7 +1714,7 @@ com.google.http-client google-http-client - 1.46.3 + 1.47.0 com.google.errorprone @@ -1736,7 +1736,7 @@ com.google.http-client google-http-client-jackson2 - 1.46.3 + 1.47.0 jackson-core @@ -1758,7 +1758,7 @@ com.google.http-client google-http-client-gson - 1.46.3 + 1.47.0 From 19010353c9293e87ae4710c4e829ba3f07a60507 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 00:15:34 +0000 Subject: [PATCH 067/701] Bump pdfbox-version from 2.0.33 to 2.0.34 Bumps `pdfbox-version` from 2.0.33 to 2.0.34. Updates `org.apache.pdfbox:pdfbox` from 2.0.33 to 2.0.34 Updates `org.apache.pdfbox:fontbox` from 2.0.33 to 2.0.34 --- updated-dependencies: - dependency-name: org.apache.pdfbox:pdfbox dependency-version: 2.0.34 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.pdfbox:fontbox dependency-version: 2.0.34 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aff93df4a23c..8b079f29ad51 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 9.4.57.v20241219 2.24.3 - 2.0.33 + 2.0.34 1.19.0 1.7.36 2.9.3 From e94d934a5ca371a66e65297fefef9cf1f7c37488 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 29 Apr 2025 17:35:19 +0200 Subject: [PATCH 068/701] Make getAllFacetConfigs unique Improve performance and debuggability by refactoring getAllFacetConfigs to getAllUniqueFacetConfigs. Used only by ChoiceAuthorityService to generate hierarchical vocabulary map for the browse menu, etc. (cherry picked from commit 159bd18529b5f0918cc0bc07e2b4fee16e4f2511) --- .../authority/ChoiceAuthorityServiceImpl.java | 2 +- .../DiscoveryConfigurationService.java | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java index f4d1f02710e1..bbe8e4461fe0 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java @@ -577,7 +577,7 @@ public DSpaceControlledVocabularyIndex getVocabularyIndex(String nameVocab) { .collect(Collectors.toList())); } DiscoverySearchFilterFacet matchingFacet = null; - for (DiscoverySearchFilterFacet facetConfig : searchConfigurationService.getAllFacetsConfig()) { + for (DiscoverySearchFilterFacet facetConfig : searchConfigurationService.getAllUniqueFacetsConfig()) { boolean coversAllFieldsFromVocab = true; for (String fieldFromVocab: metadataFields) { boolean coversFieldFromVocab = false; diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java index 6cb93e2993f3..9d603941de39 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java @@ -10,8 +10,10 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -197,15 +199,19 @@ public List getIndexAlwaysConfigurations() { } /** - * @return All configurations for {@link org.dspace.discovery.configuration.DiscoverySearchFilterFacet} + * Get the unique set of configured Discovery facets. This is used when inspecting configuration + * to include hierarchical vocabularies in the browse menu. + * + * @return All unique instances of {@link org.dspace.discovery.configuration.DiscoverySearchFilterFacet} + * included in "sidebarFacets" bean, across all Discovery configurations. */ - public List getAllFacetsConfig() { - List configs = new ArrayList<>(); + public List getAllUniqueFacetsConfig() { + Set configs = new LinkedHashSet<>(); for (String key : map.keySet()) { DiscoveryConfiguration config = map.get(key); configs.addAll(config.getSidebarFacets()); } - return configs; + return new ArrayList<>(configs); } public static void main(String[] args) { From 98921724f41b3e18d21540bb34340c751ff3edd6 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 23 Apr 2025 18:55:29 +0200 Subject: [PATCH 069/701] Add help opt and javadoc to InitializeEntities (cherry picked from commit 5240a029965807e4757316aa1a965c4102c0d4f4) --- .../dspace/app/util/InitializeEntities.java | 76 +++++++++++++++++-- 1 file changed, 68 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/InitializeEntities.java b/dspace-api/src/main/java/org/dspace/app/util/InitializeEntities.java index 0a072a9819eb..8d3964a3e3c7 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/InitializeEntities.java +++ b/dspace-api/src/main/java/org/dspace/app/util/InitializeEntities.java @@ -64,20 +64,36 @@ private InitializeEntities() { */ public static void main(String[] argv) throws SQLException, AuthorizeException, ParseException { InitializeEntities initializeEntities = new InitializeEntities(); + // Set up command-line options and parse arguments CommandLineParser parser = new DefaultParser(); Options options = createCommandLineOptions(); CommandLine line = parser.parse(options,argv); - String fileLocation = getFileLocationFromCommandLine(line); + // First of all, check if the help option was entered or a required argument is missing checkHelpEntered(options, line); + // Get the file location from the command line + String fileLocation = getFileLocationFromCommandLine(line); + // Run the script initializeEntities.run(fileLocation); } + + /** + * Check if the help option was entered or a required argument is missing. If so, print help and exit. + * @param options the defined command-line options + * @param line the parsed command-line arguments + */ private static void checkHelpEntered(Options options, CommandLine line) { - if (line.hasOption("h")) { + if (line.hasOption("h") || !line.hasOption("f")) { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("Intialize Entities", options); System.exit(0); } } + + /** + * Get the file path from the command-line argument. Exits with exit code 1 if no file argument was entered. + * @param line the parsed command-line arguments + * @return the file path + */ private static String getFileLocationFromCommandLine(CommandLine line) { String query = line.getOptionValue("f"); if (StringUtils.isEmpty(query)) { @@ -88,13 +104,25 @@ private static String getFileLocationFromCommandLine(CommandLine line) { return query; } + /** + * Create the command-line options + * @return the command-line options + */ protected static Options createCommandLineOptions() { Options options = new Options(); - options.addOption("f", "file", true, "the location for the file containing the xml data"); + options.addOption("f", "file", true, "the path to the file containing the " + + "relationship definitions (e.g. ${dspace.dir}/config/entities/relationship-types.xml)"); + options.addOption("h", "help", false, "print this message"); return options; } + /** + * Run the script for the given file location + * @param fileLocation the file location + * @throws SQLException If something goes wrong initializing context or inserting relationship types + * @throws AuthorizeException If the script user fails to authorize while inserting relationship types + */ private void run(String fileLocation) throws SQLException, AuthorizeException { Context context = new Context(); context.turnOffAuthorisationSystem(); @@ -102,6 +130,12 @@ private void run(String fileLocation) throws SQLException, AuthorizeException { context.complete(); } + /** + * Parse the XML file at fileLocation to create relationship types in the database + * @param context DSpace context + * @param fileLocation the full or relative file path to the relationship types XML + * @throws AuthorizeException If the script user fails to authorize while inserting relationship types + */ private void parseXMLToRelations(Context context, String fileLocation) throws AuthorizeException { try { File fXmlFile = new File(fileLocation); @@ -158,15 +192,15 @@ private void parseXMLToRelations(Context context, String fileLocation) throws Au for (int j = 0; j < leftCardinalityList.getLength(); j++) { Node node = leftCardinalityList.item(j); - leftCardinalityMin = getString(leftCardinalityMin,(Element) node, "min"); - leftCardinalityMax = getString(leftCardinalityMax,(Element) node, "max"); + leftCardinalityMin = getCardinalityMinString(leftCardinalityMin,(Element) node, "min"); + leftCardinalityMax = getCardinalityMinString(leftCardinalityMax,(Element) node, "max"); } for (int j = 0; j < rightCardinalityList.getLength(); j++) { Node node = rightCardinalityList.item(j); - rightCardinalityMin = getString(rightCardinalityMin,(Element) node, "min"); - rightCardinalityMax = getString(rightCardinalityMax,(Element) node, "max"); + rightCardinalityMin = getCardinalityMinString(rightCardinalityMin,(Element) node, "min"); + rightCardinalityMax = getCardinalityMinString(rightCardinalityMax,(Element) node, "max"); } populateRelationshipType(context, leftType, rightType, leftwardType, rightwardType, @@ -182,13 +216,39 @@ private void parseXMLToRelations(Context context, String fileLocation) throws Au } } - private String getString(String leftCardinalityMin,Element node, String minOrMax) { + /** + * Extract the min or max value for the left or right cardinality from the node text content + * @param leftCardinalityMin current left cardinality min + * @param node node to extract the min or max value from + * @param minOrMax element tag name to parse + * @return final left cardinality min + */ + private String getCardinalityMinString(String leftCardinalityMin, Element node, String minOrMax) { if (node.getElementsByTagName(minOrMax).getLength() > 0) { leftCardinalityMin = node.getElementsByTagName(minOrMax).item(0).getTextContent(); } return leftCardinalityMin; } + /** + * Populate the relationship type based on values parsed from the XML relationship types configuration + * + * @param context DSpace context + * @param leftType left relationship type (e.g. "Publication"). + * @param rightType right relationship type (e.g. "Journal"). + * @param leftwardType leftward relationship type (e.g. "isAuthorOfPublication"). + * @param rightwardType rightward relationship type (e.g. "isPublicationOfAuthor"). + * @param leftCardinalityMin left cardinality min + * @param leftCardinalityMax left cardinality max + * @param rightCardinalityMin right cardinality min + * @param rightCardinalityMax right cardinality max + * @param copyToLeft copy metadata values to left if right side is deleted + * @param copyToRight copy metadata values to right if left side is deleted + * @param tilted set a tilted relationship side (left or right) if there are many relationships going one way + * to help performance (e.g. authors with 1000s of publications) + * @throws SQLException if database error occurs while saving the relationship type + * @throws AuthorizeException if authorization error occurs while saving the relationship type + */ private void populateRelationshipType(Context context, String leftType, String rightType, String leftwardType, String rightwardType, String leftCardinalityMin, String leftCardinalityMax, String rightCardinalityMin, String rightCardinalityMax, From fbb496e1c616e10dc667d794e19cac84e6e04913 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 23 Apr 2025 19:02:30 +0200 Subject: [PATCH 070/701] Improve help and docs for RegistryLoader And a few other small improvements (cherry picked from commit f1b4e6ef174559f901354176f934b05e3d698d58) --- .../org/dspace/administer/RegistryLoader.java | 132 +++++++++++++----- 1 file changed, 94 insertions(+), 38 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java b/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java index bbf320a0d5e5..d503bfc00b7f 100644 --- a/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java +++ b/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java @@ -21,6 +21,13 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.BitstreamFormat; @@ -41,7 +48,7 @@ *

* RegistryLoader -bitstream bitstream-formats.xml *

- * RegistryLoader -dc dc-types.xml + * RegistryLoader -metadata dc-types.xml * * @author Robert Tansley * @version $Revision$ @@ -50,7 +57,7 @@ public class RegistryLoader { /** * log4j category */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(RegistryLoader.class); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(RegistryLoader.class); protected static BitstreamFormatService bitstreamFormatService = ContentServiceFactory.getInstance() .getBitstreamFormatService(); @@ -67,48 +74,97 @@ private RegistryLoader() { } * @throws Exception if error */ public static void main(String[] argv) throws Exception { - String usage = "Usage: " + RegistryLoader.class.getName() - + " (-bitstream | -metadata) registry-file.xml"; - - Context context = null; + // Set up command-line options and parse arguments + CommandLineParser parser = new DefaultParser(); + Options options = createCommandLineOptions(); try { - context = new Context(); + CommandLine line = parser.parse(options, argv); + + // Check if help option was entered or no options provided + if (line.hasOption('h') || line.getOptions().length == 0) { + printHelp(options); + System.exit(0); + } + + Context context = new Context(); // Can't update registries anonymously, so we need to turn off // authorisation context.turnOffAuthorisationSystem(); - // Work out what we're loading - if (argv[0].equalsIgnoreCase("-bitstream")) { - RegistryLoader.loadBitstreamFormats(context, argv[1]); - } else if (argv[0].equalsIgnoreCase("-metadata")) { - // Call MetadataImporter, as it handles Metadata schema updates - MetadataImporter.loadRegistry(argv[1], true); - } else { - System.err.println(usage); + try { + // Work out what we're loading + if (line.hasOption('b')) { + String filename = line.getOptionValue('b'); + if (StringUtils.isEmpty(filename)) { + System.err.println("No file path provided for bitstream format registry"); + printHelp(options); + System.exit(1); + } + RegistryLoader.loadBitstreamFormats(context, filename); + } else if (line.hasOption('m')) { + String filename = line.getOptionValue('m'); + if (StringUtils.isEmpty(filename)) { + System.err.println("No file path provided for metadata registry"); + printHelp(options); + System.exit(1); + } + // Call MetadataImporter, as it handles Metadata schema updates + MetadataImporter.loadRegistry(filename, true); + } else { + System.err.println("No registry type specified"); + printHelp(options); + System.exit(1); + } + + // Commit changes and close Context + context.complete(); + System.exit(0); + } catch (Exception e) { + log.fatal(LogHelper.getHeader(context, "error_loading_registries", ""), e); + System.err.println("Error: \n - " + e.getMessage()); + System.exit(1); + } finally { + // Clean up our context, if it still exists & it was never completed + if (context != null && context.isValid()) { + context.abort(); + } } + } catch (ParseException e) { + System.err.println("Error parsing command-line arguments: " + e.getMessage()); + printHelp(options); + System.exit(1); + } + } - // Commit changes and close Context - context.complete(); + /** + * Create the command-line options + * @return the command-line options + */ + private static Options createCommandLineOptions() { + Options options = new Options(); - System.exit(0); - } catch (ArrayIndexOutOfBoundsException ae) { - System.err.println(usage); + options.addOption("b", "bitstream", true, "load bitstream format registry from specified file"); + options.addOption("m", "metadata", true, "load metadata registry from specified file"); + options.addOption("h", "help", false, "print this help message"); - System.exit(1); - } catch (Exception e) { - log.fatal(LogHelper.getHeader(context, "error_loading_registries", - ""), e); + return options; + } - System.err.println("Error: \n - " + e.getMessage()); - System.exit(1); - } finally { - // Clean up our context, if it still exists & it was never completed - if (context != null && context.isValid()) { - context.abort(); - } - } + /** + * Print the help message + * @param options the command-line options + */ + private static void printHelp(Options options) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("RegistryLoader", + "Load bitstream format or metadata registries into the database\n", + options, + "\nExamples:\n" + + " RegistryLoader -b bitstream-formats.xml\n" + + " RegistryLoader -m dc-types.xml", + true); } /** @@ -221,7 +277,7 @@ private static Document loadXML(String filename) throws IOException, * contains: *

* - * <foo><mimetype>application/pdf</mimetype></foo> + * application/pdf * * passing this the foo node and mimetype will * return application/pdf. @@ -262,10 +318,10 @@ private static String getElementData(Node parentElement, String childName) * document contains: *

* - * <foo> - * <bar>val1</bar> - * <bar>val2</bar> - * </foo> + * + * val1 + * val2 + * * * passing this the foo node and bar will * return val1 and val2. @@ -295,4 +351,4 @@ private static String[] getRepeatedElementData(Node parentElement, return data; } -} +} \ No newline at end of file From 85a9e4b731982df482f88277027748a2c0b2e0d9 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 23 Apr 2025 19:22:12 +0200 Subject: [PATCH 071/701] Let Curation CLI accept uuid identifiers (cherry picked from commit 5020689095549054c987c1fe4481a37aa86f3877) --- .../main/java/org/dspace/curate/Curation.java | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index 625692a866b3..b894dcd85f03 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -24,6 +24,8 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.io.output.NullOutputStream; +import org.dspace.app.util.DSpaceObjectUtilsImpl; +import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; @@ -35,6 +37,7 @@ import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.handle.service.HandleService; import org.dspace.scripts.DSpaceRunnable; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.utils.DSpace; /** @@ -45,7 +48,9 @@ public class Curation extends DSpaceRunnable { protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); - + protected DSpaceObjectUtils dspaceObjectUtils = DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName(DSpaceObjectUtilsImpl.class.getName(), DSpaceObjectUtilsImpl.class); + HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); protected Context context; private CurationClientOptions curationClientOptions; @@ -345,9 +350,29 @@ private void initTaskLineOptionsAndCheckIfValid() { if (this.commandLine.hasOption('i')) { this.id = this.commandLine.getOptionValue('i').toLowerCase(); + DSpaceObject dso; if (!this.id.equalsIgnoreCase("all")) { - HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); - DSpaceObject dso; + // First, try to parse the id as a UUID. If that fails, treat it as a handle. + UUID uuid = null; + try { + uuid = UUID.fromString(id); + } catch (Exception e) { + // It's not a UUID, proceed to treat it as a handle. + } + if (uuid != null) { + try { + dso = dspaceObjectUtils.findDSpaceObject(context, uuid); + if (dso != null) { + // We already resolved an object, return early + return; + } + } catch (SQLException e) { + String error = "SQLException trying to find dso with uuid " + uuid; + super.handler.logError(error); + throw new RuntimeException(error, e); + } + } + // If we get here, the id is not a UUID, so we assume it's a handle. try { dso = handleService.resolveToObject(this.context, id); } catch (SQLException e) { From 6fe9af84bdb291bff8ddcf510372aab157c6b30e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 29 Apr 2025 14:51:19 -0500 Subject: [PATCH 072/701] Potential fix for code scanning alert no. 30: Resolving XML external entity in user-controlled data Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> (cherry picked from commit a0ce50b2a497dcb1711f48ad35cda14eeabf686f) --- .../pubmed/service/PubmedImportMetadataSourceServiceImpl.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java index a6cfa625bbcf..13201b8fcde3 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java @@ -234,6 +234,8 @@ private String getSingleElementValue(String src, String elementName) { try { SAXBuilder saxBuilder = new SAXBuilder(); + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false); Document document = saxBuilder.build(new StringReader(src)); Element root = document.getRootElement(); From 90ea371e0b0a12a245b094ea057bbcf4117f9849 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 29 Apr 2025 16:57:31 -0500 Subject: [PATCH 073/701] Cannot disable DTDs with PubMed, so instead disallow external entities & entity expansion (cherry picked from commit f9614c41a6ceaa54756f780164fec40a3b185483) --- .../pubmed/service/PubmedImportMetadataSourceServiceImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java index 13201b8fcde3..000ef19eaec5 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java @@ -234,8 +234,10 @@ private String getSingleElementValue(String src, String elementName) { try { SAXBuilder saxBuilder = new SAXBuilder(); - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + // Disallow external entities & entity expansion to protect against XXE attacks + // (NOTE: We receive errors if we disable all DTDs for PubMed, so this is the best we can do) saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false); + saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); Document document = saxBuilder.build(new StringReader(src)); Element root = document.getRootElement(); From c6098c0232d73144804663df2ba5eb401efa9dbb Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Tue, 29 Apr 2025 11:57:38 +0200 Subject: [PATCH 074/701] [DURACOM-357] fix Collection Admin cannot see withdrawn item metadata (cherry picked from commit 5e2bb4fb9270a22a28a39ebcbcbac3f2781f097d) --- .../app/rest/converter/ItemConverter.java | 6 +- .../dspace/app/rest/ItemRestRepositoryIT.java | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java index fc64b66e8a16..38f829be3476 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java @@ -8,7 +8,6 @@ package org.dspace.app.rest.converter; import java.sql.SQLException; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Objects; @@ -76,8 +75,9 @@ public MetadataValueList getPermissionFilteredMetadata(Context context, Item obj List returnList = new LinkedList<>(); try { if (obj.isWithdrawn() && (Objects.isNull(context) || - Objects.isNull(context.getCurrentUser()) || !authorizeService.isAdmin(context))) { - return new MetadataValueList(new ArrayList()); + Objects.isNull(context.getCurrentUser()) || + !(authorizeService.isAdmin(context) || authorizeService.isCollectionAdmin(context)))) { + return new MetadataValueList(List.of()); } if (context != null && (authorizeService.isAdmin(context) || itemService.canEdit(context, obj))) { return new MetadataValueList(fullList); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index 714ad0b419a1..48e70a8d5f1f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -422,6 +422,68 @@ public void findOneTest() throws Exception { .andExpect(jsonPath("$", publicItem1Matcher)); } + @Test + public void findOneWithdrawnAsCollectionAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Create collection admin account + EPerson collectionAdmin = EPersonBuilder.createEPerson(context) + .withEmail("collection-admin@dspace.com") + .withPassword("test") + .withCanLogin(true) + .build(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + // Create collection + Collection adminCollection = CollectionBuilder.createCollection(context, child1) + .withName("Collection Admin col") + .withAdminGroup(collectionAdmin) + .build(); + Collection noAdminCollection = + CollectionBuilder.createCollection(context, child1).withName("Collection non Admin") + .build(); + + // both items are withdrawn + Item administeredItem = ItemBuilder.createItem(context, adminCollection) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("ExtraEntry") + .withdrawn() + .build(); + + Item nonAdministeredItem = ItemBuilder.createItem(context, noAdminCollection) + .withTitle("Public item 2") + .withIssueDate("2016-02-13") + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("TestingForMore").withSubject("ExtraEntry") + .withdrawn() + .build(); + + context.restoreAuthSystemState(); + + String collectionAdmintoken = getAuthToken(collectionAdmin.getEmail(), "test"); + + // Metadata are retrieved since user is administering the item's collection + getClient(collectionAdmintoken).perform(get("/api/core/items/" + administeredItem.getID()) + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata").isNotEmpty()); + + // No metadata is retrieved since user is not administering the item's collection + getClient().perform(get("/api/core/items/" + nonAdministeredItem.getID()) + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata").isEmpty()); + + + } + @Test public void findOneFullProjectionTest() throws Exception { context.turnOffAuthorisationSystem(); From 8ca8bd4543a9727173278f9752cf635fb2abd292 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Wed, 30 Apr 2025 08:31:01 +0200 Subject: [PATCH 075/701] [DURACOM-357] improved check for authorization on objects in ItemConverter (cherry picked from commit a70dede20b9310ec85ec6f441be16c0437c796cf) --- .../main/java/org/dspace/app/rest/converter/ItemConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java index 38f829be3476..0c5a36d5fb5a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java @@ -76,7 +76,7 @@ public MetadataValueList getPermissionFilteredMetadata(Context context, Item obj try { if (obj.isWithdrawn() && (Objects.isNull(context) || Objects.isNull(context.getCurrentUser()) || - !(authorizeService.isAdmin(context) || authorizeService.isCollectionAdmin(context)))) { + !(authorizeService.isAdmin(context) || authorizeService.isAdmin(context, obj)))) { return new MetadataValueList(List.of()); } if (context != null && (authorizeService.isAdmin(context) || itemService.canEdit(context, obj))) { From 2429a0ba2932296d7a2f680743fd4106115d03a3 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Wed, 30 Apr 2025 08:56:31 +0200 Subject: [PATCH 076/701] [DURACOM-357] improved admin check (cherry picked from commit bb3935a0473a13fd1804dd6c88cc49a9196809bb) --- .../main/java/org/dspace/app/rest/converter/ItemConverter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java index 0c5a36d5fb5a..abcd707118c8 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java @@ -76,7 +76,7 @@ public MetadataValueList getPermissionFilteredMetadata(Context context, Item obj try { if (obj.isWithdrawn() && (Objects.isNull(context) || Objects.isNull(context.getCurrentUser()) || - !(authorizeService.isAdmin(context) || authorizeService.isAdmin(context, obj)))) { + !authorizeService.isAdmin(context, obj))) { return new MetadataValueList(List.of()); } if (context != null && (authorizeService.isAdmin(context) || itemService.canEdit(context, obj))) { From 4a89a68736de365d0ca93a3b22f48309f49c88d6 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Wed, 30 Apr 2025 11:33:40 +0200 Subject: [PATCH 077/701] [DURACOM-357] improved javadoc (cherry picked from commit f1cb3c3ad144444f9ee9baf699a359f19680f38c) --- .../main/java/org/dspace/app/rest/converter/ItemConverter.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java index abcd707118c8..a1e9442f7466 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemConverter.java @@ -63,6 +63,9 @@ public ItemRest convert(Item obj, Projection projection) { /** * Retrieves the metadata list filtered according to the hidden metadata configuration * When the context is null, it will return the metadatalist as for an anonymous user + * When the context is not null, it will return the full metadata list if the user + * is allowed to edit the item or if the user is an admin. Otherwise, it will + * return the metadata list filtered according to the hidden metadata configuration * Overrides the parent method to include virtual metadata * @param context The context * @param obj The object of which the filtered metadata will be retrieved From b04eb9d72513141ba6ab39a286ba8cee3221bae3 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 1 May 2025 10:52:39 -0500 Subject: [PATCH 078/701] Replace MethodNotFoundException with more appropriate UnsupportedOperationException --- .../external/ads/ADSImportMetadataSourceServiceImpl.java | 3 +-- .../service/ArXivImportMetadataSourceServiceImpl.java | 3 +-- .../cinii/CiniiImportMetadataSourceServiceImpl.java | 3 +-- .../crossref/CrossRefImportMetadataSourceServiceImpl.java | 3 +-- .../datacite/DataCiteImportMetadataSourceServiceImpl.java | 3 +-- .../PubmedEuropeMetadataSourceServiceImpl.java | 3 +-- .../service/ScieloImportMetadataSourceServiceImpl.java | 7 +++---- .../service/ScopusImportMetadataSourceServiceImpl.java | 3 +-- .../vufind/VuFindImportMetadataSourceServiceImpl.java | 3 +-- .../wos/service/WOSImportMetadataSourceServiceImpl.java | 7 +++---- .../dspace/app/rest/ADSImportMetadataSourceServiceIT.java | 3 +-- .../app/rest/CrossRefImportMetadataSourceServiceIT.java | 3 +-- .../app/rest/DataCiteImportMetadataSourceServiceIT.java | 3 +-- .../app/rest/ScieloImportMetadataSourceServiceIT.java | 5 ++--- .../app/rest/VuFindImportMetadataSourceServiceIT.java | 3 +-- 15 files changed, 20 insertions(+), 35 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java index 8fbe4ef2cf57..ca3f48da6114 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java @@ -17,7 +17,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Callable; -import javax.el.MethodNotFoundException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -99,7 +98,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for CrossRef"); + throw new UnsupportedOperationException("This method is not implemented for CrossRef"); } @Override diff --git a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java index 96689e62ba75..4369b0d48b46 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java @@ -14,7 +14,6 @@ import java.util.Collection; import java.util.List; import java.util.concurrent.Callable; -import javax.el.MethodNotFoundException; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Invocation; @@ -162,7 +161,7 @@ public String getImportSource() { @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { // FIXME: we need this method? - throw new MethodNotFoundException("This method is not implemented for ArXiv"); + throw new UnsupportedOperationException("This method is not implemented for ArXiv"); } /** diff --git a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java index 587ad5b25838..82a4b2d77968 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java @@ -19,7 +19,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; -import javax.el.MethodNotFoundException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; @@ -113,7 +112,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Cinii"); + throw new UnsupportedOperationException("This method is not implemented for Cinii"); } public String getUrl() { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java index 419f6ca8a085..88aeec8d9498 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java @@ -16,7 +16,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; -import javax.el.MethodNotFoundException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -112,7 +111,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for CrossRef"); + throw new UnsupportedOperationException("This method is not implemented for CrossRef"); } public String getID(String id) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteImportMetadataSourceServiceImpl.java index 6c65d96b375d..34405cc3eec4 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/datacite/DataCiteImportMetadataSourceServiceImpl.java @@ -13,7 +13,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import javax.el.MethodNotFoundException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -188,7 +187,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for DataCite"); + throw new UnsupportedOperationException("This method is not implemented for DataCite"); } public String getID(String query) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java index 92d7d9fbd3fe..7cd297eb2815 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java @@ -17,7 +17,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; -import javax.el.MethodNotFoundException; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpException; @@ -153,7 +152,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for PubMed Europe"); + throw new UnsupportedOperationException("This method is not implemented for PubMed Europe"); } @Override diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java index 4f83ffe978f7..87b501470b87 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scielo/service/ScieloImportMetadataSourceServiceImpl.java @@ -20,7 +20,6 @@ import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.el.MethodNotFoundException; import javax.ws.rs.BadRequestException; import org.apache.commons.collections4.CollectionUtils; @@ -99,17 +98,17 @@ public int getRecordsCount(String query) throws MetadataSourceException { @Override public int getRecordsCount(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Scielo"); + throw new UnsupportedOperationException("This method is not implemented for Scielo"); } @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Scielo"); + throw new UnsupportedOperationException("This method is not implemented for Scielo"); } @Override public Collection findMatchingRecords(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Scielo"); + throw new UnsupportedOperationException("This method is not implemented for Scielo"); } /** diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java index e61ca0528681..a3f74694becf 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java @@ -22,7 +22,6 @@ import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.el.MethodNotFoundException; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -152,7 +151,7 @@ public ImportRecord getRecord(Query query) throws MetadataSourceException { @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for Scopus"); + throw new UnsupportedOperationException("This method is not implemented for Scopus"); } @Override diff --git a/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java index a4f90fa5ba61..7eb3743d207b 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java @@ -15,7 +15,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; -import javax.el.MethodNotFoundException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -104,7 +103,7 @@ public Collection findMatchingRecords(Query query) throws Metadata @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for VuFind"); + throw new UnsupportedOperationException("This method is not implemented for VuFind"); } @Override diff --git a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java index f550b659952b..c33a2f890123 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java @@ -21,7 +21,6 @@ import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.el.MethodNotFoundException; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -109,17 +108,17 @@ public int getRecordsCount(String query) throws MetadataSourceException { @Override public int getRecordsCount(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for WOS"); + throw new UnsupportedOperationException("This method is not implemented for WOS"); } @Override public Collection findMatchingRecords(Item item) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for WOS"); + throw new UnsupportedOperationException("This method is not implemented for WOS"); } @Override public Collection findMatchingRecords(Query query) throws MetadataSourceException { - throw new MethodNotFoundException("This method is not implemented for WOS"); + throw new UnsupportedOperationException("This method is not implemented for WOS"); } /** diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ADSImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ADSImportMetadataSourceServiceIT.java index 4878cdecab83..4150ca580e49 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ADSImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ADSImportMetadataSourceServiceIT.java @@ -17,7 +17,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import javax.el.MethodNotFoundException; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -117,7 +116,7 @@ public void adsImportMetadataGetRecordsCountByQueryTest() throws Exception { } } - @Test(expected = MethodNotFoundException.class) + @Test(expected = UnsupportedOperationException.class) public void adsImportMetadataFindMatchingRecordsTest() throws Exception { context.turnOffAuthorisationSystem(); parentCommunity = CommunityBuilder.createCommunity(context) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java index a182e7d89070..27b5e6933ba7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java @@ -17,7 +17,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import javax.el.MethodNotFoundException; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -135,7 +134,7 @@ public void crossRefImportMetadataGetRecordByIdTest() throws Exception { } } - @Test(expected = MethodNotFoundException.class) + @Test(expected = UnsupportedOperationException.class) public void crossRefImportMetadataFindMatchingRecordsTest() throws Exception { context.turnOffAuthorisationSystem(); parentCommunity = CommunityBuilder.createCommunity(context) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DataCiteImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DataCiteImportMetadataSourceServiceIT.java index c1481f0573c4..a8cd59a1afc4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DataCiteImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DataCiteImportMetadataSourceServiceIT.java @@ -15,7 +15,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import javax.el.MethodNotFoundException; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -90,7 +89,7 @@ public void dataCiteImportMetadataGetRecordsCountTest() throws Exception { } } - @Test(expected = MethodNotFoundException.class) + @Test(expected = UnsupportedOperationException.class) public void dataCiteImportMetadataFindMatchingRecordsTest() throws Exception { context.turnOffAuthorisationSystem(); parentCommunity = CommunityBuilder.createCommunity(context) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScieloImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScieloImportMetadataSourceServiceIT.java index aafc75a065a1..af23ff4d14a4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScieloImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScieloImportMetadataSourceServiceIT.java @@ -17,7 +17,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import javax.el.MethodNotFoundException; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -92,7 +91,7 @@ public void scieloImportMetadataGetRecordsCountTest() throws Exception { } } - @Test(expected = MethodNotFoundException.class) + @Test(expected = UnsupportedOperationException.class) public void scieloImportMetadataFindMatchingRecordsTest() throws Exception { context.turnOffAuthorisationSystem(); parentCommunity = CommunityBuilder.createCommunity(context) @@ -111,7 +110,7 @@ public void scieloImportMetadataFindMatchingRecordsTest() throws Exception { scieloServiceImpl.findMatchingRecords(testItem); } - @Test(expected = MethodNotFoundException.class) + @Test(expected = UnsupportedOperationException.class) public void scieloImportMetadataGetRecordsCountByQueryTest() throws Exception { Query q = new Query(); q.addParameter("query", "test query"); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VuFindImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VuFindImportMetadataSourceServiceIT.java index c3063ca23459..89b67bbb4aa2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VuFindImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VuFindImportMetadataSourceServiceIT.java @@ -17,7 +17,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import javax.el.MethodNotFoundException; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -119,7 +118,7 @@ public void vuFindImportMetadataGetRecordByIdTest() throws Exception { } } - @Test(expected = MethodNotFoundException.class) + @Test(expected = UnsupportedOperationException.class) public void vuFindImportMetadataFindMatchingRecordsTest() throws Exception { context.turnOffAuthorisationSystem(); parentCommunity = CommunityBuilder.createCommunity(context) From d83779022e3d62ea45539a8b051d869a3b19743d Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 1 May 2025 12:53:41 -0500 Subject: [PATCH 079/701] Remove javax.el dependency --- dspace-api/pom.xml | 5 ----- pom.xml | 6 ------ 2 files changed, 11 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 3de7099ad668..bb246c20ad8f 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -505,11 +505,6 @@ javax.servlet-api provided - - - javax.el - javax.el-api - javax.annotation javax.annotation-api diff --git a/pom.xml b/pom.xml index a58392cd80c1..e2683a5844b7 100644 --- a/pom.xml +++ b/pom.xml @@ -1554,12 +1554,6 @@ javax.servlet-api 3.1.0 - - - javax.el - javax.el-api - 3.0.0 - From 4ec611bf796ac58f60c53f808facfb3211810cff Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Tue, 19 Nov 2024 12:13:37 +0100 Subject: [PATCH 080/701] 119612: configurable limit on exporting items since it can take up a bunch of resources (cherry picked from commit b634e1e38070e65128c49d702affa3b19842ebb6) --- .../app/bulkedit/MetadataExportSearch.java | 2 +- .../MetadataDSpaceCsvExportServiceImpl.java | 43 +++++++++++++++++-- .../MetadataDSpaceCsvExportService.java | 6 ++- .../MetadataDSpaceCsvExportServiceImplIT.java | 8 +++- dspace/config/dspace.cfg | 9 ++++ 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java index 027ad116a7e2..e4bbe335d63e 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java @@ -143,7 +143,7 @@ public void internalRun() throws Exception { Iterator itemIterator = searchService.iteratorSearch(context, dso, discoverQuery); handler.logDebug("creating dspacecsv"); - DSpaceCSV dSpaceCSV = metadataDSpaceCsvExportService.export(context, itemIterator, true); + DSpaceCSV dSpaceCSV = metadataDSpaceCsvExportService.export(context, itemIterator, true, handler); handler.logDebug("writing to file " + getFileNameOrExportFile()); handler.writeFilestream(context, getFileNameOrExportFile(), dSpaceCSV.getInputStream(), EXPORT_CSV); context.restoreAuthSystemState(); diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java index 8bc34d3f5ed1..7e313b79511e 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java @@ -15,14 +15,17 @@ import java.util.Set; import java.util.UUID; +import org.apache.commons.collections.IteratorUtils; import org.dspace.app.bulkedit.DSpaceCSV; import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.content.service.ItemService; import org.dspace.content.service.MetadataDSpaceCsvExportService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.eperson.service.GroupService; import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.scripts.handler.DSpaceRunnableHandler; +import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; /** @@ -36,6 +39,12 @@ public class MetadataDSpaceCsvExportServiceImpl implements MetadataDSpaceCsvExpo @Autowired private DSpaceObjectUtils dSpaceObjectUtils; + @Autowired + private ConfigurationService configurationService; + + @Autowired + private GroupService groupService; + @Override public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean exportAllMetadata, String identifier, DSpaceRunnableHandler handler) throws Exception { @@ -74,17 +83,19 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e } } - DSpaceCSV csv = this.export(context, toExport, exportAllMetadata); + DSpaceCSV csv = this.export(context, toExport, exportAllMetadata, handler); return csv; } @Override - public DSpaceCSV export(Context context, Iterator toExport, boolean exportAll) throws Exception { + public DSpaceCSV export(Context context, Iterator toExport, + boolean exportAll, DSpaceRunnableHandler handler) throws Exception { Context.Mode originalMode = context.getCurrentMode(); context.setMode(Context.Mode.READ_ONLY); // Process each item DSpaceCSV csv = new DSpaceCSV(exportAll); + toExport = setItemsToExportWithLimit(context, toExport, handler); while (toExport.hasNext()) { Item item = toExport.next(); csv.addItem(item); @@ -97,8 +108,32 @@ public DSpaceCSV export(Context context, Iterator toExport, boolean export } @Override - public DSpaceCSV export(Context context, Community community, boolean exportAll) throws Exception { - return export(context, buildFromCommunity(context, community), exportAll); + public DSpaceCSV export(Context context, Community community, + boolean exportAll, DSpaceRunnableHandler handler) throws Exception { + return export(context, buildFromCommunity(context, community), exportAll, handler); + } + + private Iterator setItemsToExportWithLimit(Context context, Iterator toExport, + DSpaceRunnableHandler handler) throws SQLException { + int itemExportLimit = configurationService.getIntProperty( + "metadataexport.max.items", 500); + String[] ignoreLimitGroups = configurationService.getArrayProperty( + "metadataexport.admin.groups"); + + for (String group : ignoreLimitGroups) { + if (groupService.isMember(context, context.getCurrentUser(), group)) { + itemExportLimit = Integer.MAX_VALUE; + break; + } + } + + List items = IteratorUtils.toList(toExport); + if (items.size() > itemExportLimit) { + handler.logWarning("The amount of items to export is higher than the limit of " + itemExportLimit + + " items. Only the first " + itemExportLimit + " items will be exported."); + items = items.subList(0, itemExportLimit); + } + return items.iterator(); } /** diff --git a/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java b/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java index d3fc2e823669..052754a16331 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java @@ -44,7 +44,8 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e * @return A DSpaceCSV object containing the exported information * @throws Exception If something goes wrong */ - public DSpaceCSV export(Context context, Iterator toExport, boolean exportAll) throws Exception; + public DSpaceCSV export(Context context, Iterator toExport, + boolean exportAll, DSpaceRunnableHandler handler) throws Exception; /** * This method will export all the Items within the given Community to a DSpaceCSV @@ -54,6 +55,7 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e * @return A DSpaceCSV object containing the exported information * @throws Exception If something goes wrong */ - public DSpaceCSV export(Context context, Community community, boolean exportAll) throws Exception; + public DSpaceCSV export(Context context, Community community, + boolean exportAll, DSpaceRunnableHandler handler) throws Exception; } \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/content/MetadataDSpaceCsvExportServiceImplIT.java b/dspace-api/src/test/java/org/dspace/content/MetadataDSpaceCsvExportServiceImplIT.java index c2d4f56ca61a..5a3412646482 100644 --- a/dspace-api/src/test/java/org/dspace/content/MetadataDSpaceCsvExportServiceImplIT.java +++ b/dspace-api/src/test/java/org/dspace/content/MetadataDSpaceCsvExportServiceImplIT.java @@ -16,6 +16,7 @@ import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.app.bulkedit.DSpaceCSV; import org.dspace.app.bulkedit.DSpaceCSVLine; +import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.ItemBuilder; @@ -31,6 +32,9 @@ */ public class MetadataDSpaceCsvExportServiceImplIT extends AbstractIntegrationTestWithDatabase { + + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + /** * Test of handleExport method, of class MetadataDSpaceCsvExportServiceImpl. * @throws java.lang.Exception passed through. @@ -66,7 +70,7 @@ public void testExport_3args_1() boolean exportAll = false; MetadataDSpaceCsvExportServiceImpl instance = new MetadataDSpaceCsvExportServiceImpl(); DSpaceCSV expResult = null; - DSpaceCSV result = instance.export(context, toExport, exportAll); + DSpaceCSV result = instance.export(context, toExport, exportAll, testDSpaceRunnableHandler); assertEquals(expResult, result); // TODO review the generated test code and remove the default call to fail. fail("The test case is a prototype."); @@ -105,7 +109,7 @@ public void testMappedItem() .getServiceManager() .getServiceByName(MetadataDSpaceCsvExportServiceImpl.class.getCanonicalName(), MetadataDSpaceCsvExportService.class); - DSpaceCSV result = instance.export(context, parentCommunity, false); + DSpaceCSV result = instance.export(context, parentCommunity, false, testDSpaceRunnableHandler); // Examine the result. List csvLines = result.getCSVLines(); diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 94b731cd3488..32598bd8ecdb 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -893,6 +893,15 @@ org.dspace.app.itemexport.life.span.hours = 48 # cumulative sizes are more than this entry the export is not kicked off org.dspace.app.itemexport.max.size = 200 +### Bulkedit Metadata export settings +# The maximum amount of items that can be exported using the "metadata-export" script / process +# Recommend to keep this at a feasible number, as exporting large amounts of items can be resource intensive +metadataexport.max.items = 1 + +# A list of groups that are allowed to use the metadata-export script without any restrictions +#metadataexport.admin.groups = Administrator + + ### Batch Item import settings ### # The directory where the results of imports will be placed (mapfile, upload file) org.dspace.app.batchitemimport.work.dir = ${dspace.dir}/imports From 4626b06d7fd95da1b805ef879d174c155ba65650 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Wed, 20 Nov 2024 17:09:38 +0100 Subject: [PATCH 081/701] 119612: property should be commented by default and have a normal limit (cherry picked from commit a8b98bb7b78c9220e159c587a55def1bff3cb023) --- dspace/config/dspace.cfg | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 32598bd8ecdb..023bc45ff7c1 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -894,9 +894,10 @@ org.dspace.app.itemexport.life.span.hours = 48 org.dspace.app.itemexport.max.size = 200 ### Bulkedit Metadata export settings -# The maximum amount of items that can be exported using the "metadata-export" script / process +# The maximum amount of items that can be exported using the "metadata-export" / "metadata-export-search" script # Recommend to keep this at a feasible number, as exporting large amounts of items can be resource intensive -metadataexport.max.items = 1 +# If not set, this will default to 500 items +# metadataexport.max.items = 500 # A list of groups that are allowed to use the metadata-export script without any restrictions #metadataexport.admin.groups = Administrator From c5c8417848d7d59f975e32144b99b5fa83549748 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Mon, 20 Jan 2025 15:46:26 +0100 Subject: [PATCH 082/701] 119612: Remove group configuration and expose property to angular (cherry picked from commit c73c739deb007c657641f198414da886c2953807) --- .../content/MetadataDSpaceCsvExportServiceImpl.java | 13 ------------- dspace/config/dspace.cfg | 3 --- dspace/config/modules/rest.cfg | 1 + 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java index 7e313b79511e..3dd396589344 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java @@ -22,7 +22,6 @@ import org.dspace.content.service.MetadataDSpaceCsvExportService; import org.dspace.core.Constants; import org.dspace.core.Context; -import org.dspace.eperson.service.GroupService; import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.scripts.handler.DSpaceRunnableHandler; import org.dspace.services.ConfigurationService; @@ -42,9 +41,6 @@ public class MetadataDSpaceCsvExportServiceImpl implements MetadataDSpaceCsvExpo @Autowired private ConfigurationService configurationService; - @Autowired - private GroupService groupService; - @Override public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean exportAllMetadata, String identifier, DSpaceRunnableHandler handler) throws Exception { @@ -117,15 +113,6 @@ private Iterator setItemsToExportWithLimit(Context context, Iterator DSpaceRunnableHandler handler) throws SQLException { int itemExportLimit = configurationService.getIntProperty( "metadataexport.max.items", 500); - String[] ignoreLimitGroups = configurationService.getArrayProperty( - "metadataexport.admin.groups"); - - for (String group : ignoreLimitGroups) { - if (groupService.isMember(context, context.getCurrentUser(), group)) { - itemExportLimit = Integer.MAX_VALUE; - break; - } - } List items = IteratorUtils.toList(toExport); if (items.size() > itemExportLimit) { diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 023bc45ff7c1..2e5c06c707c3 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -899,9 +899,6 @@ org.dspace.app.itemexport.max.size = 200 # If not set, this will default to 500 items # metadataexport.max.items = 500 -# A list of groups that are allowed to use the metadata-export script without any restrictions -#metadataexport.admin.groups = Administrator - ### Batch Item import settings ### # The directory where the results of imports will be placed (mapfile, upload file) diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 537eedbd087b..86f0b801dbd5 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -55,6 +55,7 @@ rest.properties.exposed = cc.license.jurisdiction rest.properties.exposed = identifiers.item-status.register-doi rest.properties.exposed = authentication-password.domain.valid rest.properties.exposed = handle.canonical.prefix +rest.properties.exposed = metadataexport.max.items #---------------------------------------------------------------# # These configs are used by the deprecated REST (v4-6) module # From 5929fdc926a5bd3ae5df08bd69860b4562490614 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Mon, 3 Feb 2025 10:09:47 +0100 Subject: [PATCH 083/701] 124504: Move configuration to be included in the bulkedit module and apply the configured limit earlier, never obtaining a larger list than actually required (cherry picked from commit b63ffd2eb4a869d17d2caca2d726d7702b05d564) --- .../app/bulkedit/MetadataExportSearch.java | 2 + .../MetadataDSpaceCsvExportServiceImpl.java | 42 +++++++++---------- .../MetadataDSpaceCsvExportService.java | 4 +- dspace/config/dspace.cfg | 7 ---- dspace/config/modules/bulkedit.cfg | 5 +++ dspace/config/modules/rest.cfg | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java index e4bbe335d63e..04c3a75dedd6 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java @@ -139,6 +139,8 @@ public void internalRun() throws Exception { DiscoverQuery discoverQuery = queryBuilder.buildQuery(context, dso, discoveryConfiguration, query, queryBuilderSearchFilters, "Item", 10, Long.getLong("0"), null, SortOption.DESCENDING); + // add configured limit + discoverQuery.setMaxResults(metadataDSpaceCsvExportService.getCsvExportLimit()); handler.logDebug("creating iterator"); Iterator itemIterator = searchService.iteratorSearch(context, dso, discoverQuery); diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java index 3dd396589344..33e87a43fcd4 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java @@ -15,7 +15,6 @@ import java.util.Set; import java.util.UUID; -import org.apache.commons.collections.IteratorUtils; import org.dspace.app.bulkedit.DSpaceCSV; import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.content.service.ItemService; @@ -41,6 +40,8 @@ public class MetadataDSpaceCsvExportServiceImpl implements MetadataDSpaceCsvExpo @Autowired private ConfigurationService configurationService; + private int csxExportLimit = -1; + @Override public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean exportAllMetadata, String identifier, DSpaceRunnableHandler handler) throws Exception { @@ -48,7 +49,7 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e if (exportAllItems) { handler.logInfo("Exporting whole repository WARNING: May take some time!"); - toExport = itemService.findAll(context); + toExport = itemService.findAll(context, getCsvExportLimit(), 0); } else { DSpaceObject dso = HandleServiceFactory.getInstance().getHandleService() .resolveToObject(context, identifier); @@ -68,7 +69,7 @@ public DSpaceCSV handleExport(Context context, boolean exportAllItems, boolean e } else if (dso.getType() == Constants.COLLECTION) { handler.logInfo("Exporting collection '" + dso.getName() + "' (" + identifier + ")"); Collection collection = (Collection) dso; - toExport = itemService.findByCollection(context, collection); + toExport = itemService.findByCollection(context, collection, getCsvExportLimit(), 0); } else if (dso.getType() == Constants.COMMUNITY) { handler.logInfo("Exporting community '" + dso.getName() + "' (" + identifier + ")"); toExport = buildFromCommunity(context, (Community) dso); @@ -91,7 +92,6 @@ public DSpaceCSV export(Context context, Iterator toExport, // Process each item DSpaceCSV csv = new DSpaceCSV(exportAll); - toExport = setItemsToExportWithLimit(context, toExport, handler); while (toExport.hasNext()) { Item item = toExport.next(); csv.addItem(item); @@ -109,20 +109,6 @@ public DSpaceCSV export(Context context, Community community, return export(context, buildFromCommunity(context, community), exportAll, handler); } - private Iterator setItemsToExportWithLimit(Context context, Iterator toExport, - DSpaceRunnableHandler handler) throws SQLException { - int itemExportLimit = configurationService.getIntProperty( - "metadataexport.max.items", 500); - - List items = IteratorUtils.toList(toExport); - if (items.size() > itemExportLimit) { - handler.logWarning("The amount of items to export is higher than the limit of " + itemExportLimit - + " items. Only the first " + itemExportLimit + " items will be exported."); - items = items.subList(0, itemExportLimit); - } - return items.iterator(); - } - /** * Build a Java Collection of item IDs that are in a Community (including * its sub-Communities and Collections) @@ -135,25 +121,37 @@ private Iterator setItemsToExportWithLimit(Context context, Iterator private Iterator buildFromCommunity(Context context, Community community) throws SQLException { Set result = new HashSet<>(); + int itemsAdded = 0; // Add all the collections List collections = community.getCollections(); for (Collection collection : collections) { - Iterator items = itemService.findByCollection(context, collection); - while (items.hasNext()) { + // Never obtain more items than the configured limit + Iterator items = itemService.findByCollection(context, collection, getCsvExportLimit(), 0); + while (itemsAdded <= getCsvExportLimit() && items.hasNext()) { result.add(items.next()); + itemsAdded++; } } - // Add all the sub-communities + // Add all the sub-communities List communities = community.getSubcommunities(); for (Community subCommunity : communities) { Iterator items = buildFromCommunity(context, subCommunity); - while (items.hasNext()) { + while (itemsAdded <= getCsvExportLimit() && items.hasNext()) { result.add(items.next()); + itemsAdded++; } } return result.iterator(); } + + @Override + public int getCsvExportLimit() { + if (csxExportLimit == -1) { + csxExportLimit = configurationService.getIntProperty("bulkedit.export.max.items", 500); + } + return csxExportLimit; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java b/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java index 052754a16331..a951e8cf7763 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/MetadataDSpaceCsvExportService.java @@ -58,4 +58,6 @@ public DSpaceCSV export(Context context, Iterator toExport, public DSpaceCSV export(Context context, Community community, boolean exportAll, DSpaceRunnableHandler handler) throws Exception; -} \ No newline at end of file + int getCsvExportLimit(); + +} diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 2e5c06c707c3..94b731cd3488 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -893,13 +893,6 @@ org.dspace.app.itemexport.life.span.hours = 48 # cumulative sizes are more than this entry the export is not kicked off org.dspace.app.itemexport.max.size = 200 -### Bulkedit Metadata export settings -# The maximum amount of items that can be exported using the "metadata-export" / "metadata-export-search" script -# Recommend to keep this at a feasible number, as exporting large amounts of items can be resource intensive -# If not set, this will default to 500 items -# metadataexport.max.items = 500 - - ### Batch Item import settings ### # The directory where the results of imports will be placed (mapfile, upload file) org.dspace.app.batchitemimport.work.dir = ${dspace.dir}/imports diff --git a/dspace/config/modules/bulkedit.cfg b/dspace/config/modules/bulkedit.cfg index 6174af53a0f3..e326e007f884 100644 --- a/dspace/config/modules/bulkedit.cfg +++ b/dspace/config/modules/bulkedit.cfg @@ -40,3 +40,8 @@ bulkedit.allow-bulk-deletion = dspace.agreements.end-user # By default this is set to 100 bulkedit.change.commit.count = 100 +### Bulkedit Metadata export settings +# The maximum amount of items that can be exported using the "metadata-export" / "metadata-export-search" script +# Recommend to keep this at a feasible number, as exporting large amounts of items can be resource intensive +# If not set, this will default to 500 items +# bulkedit.export.max.items = 500 diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 86f0b801dbd5..b123b5b654b6 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -55,7 +55,7 @@ rest.properties.exposed = cc.license.jurisdiction rest.properties.exposed = identifiers.item-status.register-doi rest.properties.exposed = authentication-password.domain.valid rest.properties.exposed = handle.canonical.prefix -rest.properties.exposed = metadataexport.max.items +rest.properties.exposed = bulkedit.export.max.items #---------------------------------------------------------------# # These configs are used by the deprecated REST (v4-6) module # From fae4130d41212972358b8290b1bcacfa6fb82f65 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Wed, 2 Apr 2025 11:09:45 +0200 Subject: [PATCH 084/701] 119612: Fix limit not applying on export (cherry picked from commit bcf48821d94d30cee10c3c2c94b33c52b0bccdb0) --- .../dspace/app/bulkedit/MetadataExportSearch.java | 2 -- .../content/MetadataDSpaceCsvExportServiceImpl.java | 13 ++++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java index 04c3a75dedd6..e4bbe335d63e 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java @@ -139,8 +139,6 @@ public void internalRun() throws Exception { DiscoverQuery discoverQuery = queryBuilder.buildQuery(context, dso, discoveryConfiguration, query, queryBuilderSearchFilters, "Item", 10, Long.getLong("0"), null, SortOption.DESCENDING); - // add configured limit - discoverQuery.setMaxResults(metadataDSpaceCsvExportService.getCsvExportLimit()); handler.logDebug("creating iterator"); Iterator itemIterator = searchService.iteratorSearch(context, dso, discoverQuery); diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java index 33e87a43fcd4..757c5d0cd529 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataDSpaceCsvExportServiceImpl.java @@ -90,9 +90,11 @@ public DSpaceCSV export(Context context, Iterator toExport, Context.Mode originalMode = context.getCurrentMode(); context.setMode(Context.Mode.READ_ONLY); - // Process each item + // Process each item until we reach the limit + int itemExportLimit = getCsvExportLimit(); DSpaceCSV csv = new DSpaceCSV(exportAll); - while (toExport.hasNext()) { + + for (int itemsAdded = 0; toExport.hasNext() && itemsAdded < itemExportLimit; itemsAdded++) { Item item = toExport.next(); csv.addItem(item); context.uncacheEntity(item); @@ -121,16 +123,14 @@ public DSpaceCSV export(Context context, Community community, private Iterator buildFromCommunity(Context context, Community community) throws SQLException { Set result = new HashSet<>(); - int itemsAdded = 0; // Add all the collections List collections = community.getCollections(); for (Collection collection : collections) { // Never obtain more items than the configured limit Iterator items = itemService.findByCollection(context, collection, getCsvExportLimit(), 0); - while (itemsAdded <= getCsvExportLimit() && items.hasNext()) { + while (result.size() < getCsvExportLimit() && items.hasNext()) { result.add(items.next()); - itemsAdded++; } } @@ -138,9 +138,8 @@ private Iterator buildFromCommunity(Context context, Community community) List communities = community.getSubcommunities(); for (Community subCommunity : communities) { Iterator items = buildFromCommunity(context, subCommunity); - while (itemsAdded <= getCsvExportLimit() && items.hasNext()) { + while (result.size() < getCsvExportLimit() && items.hasNext()) { result.add(items.next()); - itemsAdded++; } } From eb5f09f3f21180afeb7e52ff62ab409368fab3c2 Mon Sep 17 00:00:00 2001 From: Adamo Date: Fri, 2 May 2025 12:42:47 +0200 Subject: [PATCH 085/701] [DURACOM-355] Update to avoid NPE during WOS live import when no api key is found (cherry picked from commit 70f1c83bf0a7b32c10eaf22c5cc990c6b10e5784) --- .../WOSImportMetadataSourceServiceImpl.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java index c33a2f890123..9bffa2a84a19 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java @@ -56,7 +56,7 @@ public class WOSImportMetadataSourceServiceImpl extends AbstractImportMetadataSo private static final String AI_PATTERN = "^AI=(.*)"; private static final Pattern ISI_PATTERN = Pattern.compile("^\\d{15}$"); - private int timeout = 1000; + private final int timeout = 1000; private String url; private String urlSearch; @@ -126,7 +126,7 @@ public Collection findMatchingRecords(Query query) throws Metadata */ private class SearchNBByQueryCallable implements Callable { - private String query; + private final String query; private SearchNBByQueryCallable(String queryString) { this.query = queryString; @@ -155,7 +155,8 @@ public Integer call() throws Exception { Element tot = xpath.evaluateFirst(root); return Integer.valueOf(tot.getValue()); } - return null; + log.warn("API key is missing: cannot execute count request."); + return 0; } } @@ -166,7 +167,7 @@ public Integer call() throws Exception { */ private class FindByIdCallable implements Callable> { - private String doi; + private final String doi; private FindByIdCallable(String doi) { this.doi = URLEncoder.encode(doi, StandardCharsets.UTF_8); @@ -185,6 +186,8 @@ public List call() throws Exception { for (Element record : elements) { results.add(transformSourceRecords(record)); } + } else { + log.warn("API key is missing: cannot execute live import request."); } return results; } @@ -202,7 +205,7 @@ public List call() throws Exception { */ private class SearchByQueryCallable implements Callable> { - private Query query; + private final Query query; private SearchByQueryCallable(String queryString, Integer maxResult, Integer start) { query = new Query(); @@ -232,6 +235,8 @@ public List call() throws Exception { for (Element el : omElements) { results.add(transformSourceRecords(el)); } + } else { + log.warn("API key is missing: cannot execute live import request."); } return results; } @@ -270,9 +275,7 @@ private String checkQuery(String query) { } else if (isIsi(query)) { return "UT=(" + query + ")"; } - StringBuilder queryBuilder = new StringBuilder("TS=("); - queryBuilder.append(query).append(")"); - return queryBuilder.toString(); + return "TS=(" + query + ")"; } private boolean isIsi(String query) { From 605956b0739372decfc398d1300a0ff3620b4b6c Mon Sep 17 00:00:00 2001 From: Adamo Date: Fri, 2 May 2025 18:08:15 +0200 Subject: [PATCH 086/701] [DURACOM-311] Ensure stable pagination in bulk access control by adding explicit sort (cherry picked from commit ced9e9b9f731162c43d5823a9d2fd05e74ab233a) --- .../bulkaccesscontrol/BulkAccessControl.java | 2 +- .../BulkAccessControlIT.java | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java index 7bef232f0450..30f68efaf3cb 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java @@ -416,7 +416,7 @@ private DiscoverQuery buildDiscoveryQuery(String query, int start, int limit) { discoverQuery.setQuery(query); discoverQuery.setStart(start); discoverQuery.setMaxResults(limit); - + discoverQuery.setSortField("search.resourceid", DiscoverQuery.SORT_ORDER.asc); return discoverQuery; } diff --git a/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java b/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java index 73f02e40494c..106b176ee5b4 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java @@ -32,8 +32,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -1831,6 +1833,88 @@ public void performBulkAccessWithHelpParamTest() throws Exception { assertThat(testDSpaceRunnableHandler.getWarningMessages(), empty()); } + + @Test + public void bulkAccessControlShouldProcessEachItemOnceWithPagination() throws Exception { + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context) + .withName("Test Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Test Collection") + .build(); + + List createdItemIDs = new ArrayList<>(); + + for (int i = 0; i < 25; i++) { + Item item = ItemBuilder.createItem(context, collection).build(); + + Bundle bundle = BundleBuilder.createBundle(context, item) + .withName("ORIGINAL") + .build(); + + BitstreamBuilder.createBitstream(context, bundle, + IOUtils.toInputStream("Bitstream content " + i, + CharEncoding.UTF_8)) + .withName("bitstream_" + i) + .build(); + + createdItemIDs.add(item.getID()); + } + + context.restoreAuthSystemState(); + + // JSON without constraints: apply to ALL items + String json = """ + { "item": { + "mode": "add", + "accessConditions": [ + { + "name": "openaccess" + } + ] + }} + """; + + buildJsonFile(json); + + String[] args = { + "bulk-access-control", + "-u", community.getID().toString(), + "-f", tempFilePath, + "-e", admin.getEmail() + }; + + TestDSpaceRunnableHandler testHandler = new TestDSpaceRunnableHandler(); + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), testHandler, kernelImpl); + + assertThat(testHandler.getErrorMessages(), empty()); + assertThat(testHandler.getWarningMessages(), empty()); + + // Collect item IDs from the info messages + List infoMessages = testHandler.getInfoMessages(); + List updatedItemIDs = infoMessages.stream() + .map(msg -> { + int startIdx = msg.indexOf("Item {") + 6; + int endIdx = msg.indexOf("}", startIdx); + return UUID.fromString(msg.substring(startIdx, endIdx)); + }) + .toList(); + + Set uniqueUpdatedItemIDs = new HashSet<>(updatedItemIDs); + + // Check if any item was processed multiple times + assertThat("Some items were processed more than once!", + uniqueUpdatedItemIDs.size(), is(updatedItemIDs.size())); + + // Check all items were updated once + assertThat("Not all created items were updated!", + createdItemIDs, containsInAnyOrder(uniqueUpdatedItemIDs.toArray())); + } + + private List findItems(String query) throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); From 3f9f5639f64e32ebc56c521f3c67e913fca35de3 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 2 May 2025 15:49:00 -0500 Subject: [PATCH 087/701] Must use older Java syntax in DSpace 7 --- .../BulkAccessControlIT.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java b/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java index 106b176ee5b4..0dd1bf076169 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkaccesscontrol/BulkAccessControlIT.java @@ -1867,16 +1867,16 @@ public void bulkAccessControlShouldProcessEachItemOnceWithPagination() throws Ex context.restoreAuthSystemState(); // JSON without constraints: apply to ALL items - String json = """ - { "item": { - "mode": "add", - "accessConditions": [ - { - "name": "openaccess" - } - ] - }} - """; + String json = "{\n" + + " \"item\": {\n" + + " \"mode\": \"add\",\n" + + " \"accessConditions\": [\n" + + " {\n" + + " \"name\": \"openaccess\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; buildJsonFile(json); @@ -1901,7 +1901,7 @@ public void bulkAccessControlShouldProcessEachItemOnceWithPagination() throws Ex int endIdx = msg.indexOf("}", startIdx); return UUID.fromString(msg.substring(startIdx, endIdx)); }) - .toList(); + .collect(Collectors.toList()); Set uniqueUpdatedItemIDs = new HashSet<>(updatedItemIDs); From a0c2891226156e749e0806ae6728fe92abc313e4 Mon Sep 17 00:00:00 2001 From: Adamo Date: Thu, 1 May 2025 18:52:26 +0200 Subject: [PATCH 088/701] [DURACOM-356] Updated Sherpa mapping to use creativeworkseries.issn instead of dc.identifier.issn --- .../provider/impl/SHERPAv2JournalISSNDataProvider.java | 3 +-- dspace/config/spring/api/sherpa.xml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalISSNDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalISSNDataProvider.java index 9e61b9ac2ac0..860334847ccb 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalISSNDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalISSNDataProvider.java @@ -106,8 +106,7 @@ private ExternalDataObject constructExternalDataObjectFromSherpaJournal(SHERPAJo String issn = sherpaJournal.getIssns().get(0); externalDataObject.setId(issn); externalDataObject.addMetadata(new MetadataValueDTO( - "dc", "identifier", "issn", null, issn)); - + "creativeworkseries", "issn", null, null, issn)); } log.debug("New external data object. Title=" + externalDataObject.getValue() + ". ID=" diff --git a/dspace/config/spring/api/sherpa.xml b/dspace/config/spring/api/sherpa.xml index 0414f3f8e4b4..da42880218ca 100644 --- a/dspace/config/spring/api/sherpa.xml +++ b/dspace/config/spring/api/sherpa.xml @@ -17,7 +17,7 @@ - dc.identifier.issn + creativeworkseries.issn From bacbe06b2c8b121eb3877b4b9520a5d5726dc381 Mon Sep 17 00:00:00 2001 From: Adamo Date: Fri, 2 May 2025 19:23:37 +0200 Subject: [PATCH 089/701] [DURACOM-356] Fixed tests --- .../org/dspace/app/sherpa/SHERPADataProviderTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java b/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java index 6b9666c83038..134561f65e5a 100644 --- a/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java @@ -86,9 +86,8 @@ public void testGetJournalISSNExternalObject() { if (metadataValue.getSchema().equalsIgnoreCase("dc") && metadataValue.getElement().equalsIgnoreCase("title")) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("issn")) { + } else if (metadataValue.getSchema().equalsIgnoreCase("creativeworkseries") + && metadataValue.getElement().equalsIgnoreCase("issn")) { identifier = metadataValue.getValue(); } } @@ -135,9 +134,8 @@ public void testSearchJournalISSNExternalObjects() { if (metadataValue.getSchema().equalsIgnoreCase("dc") && metadataValue.getElement().equalsIgnoreCase("title")) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("issn")) { + } else if (metadataValue.getSchema().equalsIgnoreCase("creativeworkseries") + && metadataValue.getElement().equalsIgnoreCase("issn")) { identifier = metadataValue.getValue(); } } From 5dd4e4248c847927fda47cb89c984eba4e055400 Mon Sep 17 00:00:00 2001 From: Adamo Date: Mon, 5 May 2025 12:46:52 +0200 Subject: [PATCH 090/701] [DURACOM-356] Updated Sherpa Journal mapping to use creativeworkseries.issn instead of dc.identifier.issn --- .../external/provider/impl/SHERPAv2JournalDataProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalDataProvider.java index a4276c83ed70..f0ce6a979a1f 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/SHERPAv2JournalDataProvider.java @@ -97,7 +97,7 @@ private ExternalDataObject constructExternalDataObjectFromSherpaJournal(SHERPAJo if (CollectionUtils.isNotEmpty(sherpaJournal.getIssns())) { String issn = sherpaJournal.getIssns().get(0); externalDataObject.addMetadata(new MetadataValueDTO( - "dc", "identifier", "issn", null, issn)); + "creativeworkseries", "issn", null, null, issn)); } From d7948d5f7bb36f23221844bc81fee571faa6b419 Mon Sep 17 00:00:00 2001 From: Adamo Date: Mon, 5 May 2025 12:49:16 +0200 Subject: [PATCH 091/701] [DURACOM-356] Updated tests to use metadata constants --- .../app/sherpa/SHERPADataProviderTest.java | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java b/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java index 134561f65e5a..210eedc9fbb4 100644 --- a/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/app/sherpa/SHERPADataProviderTest.java @@ -37,6 +37,12 @@ public class SHERPADataProviderTest extends AbstractDSpaceTest { ExternalDataProvider sherpaPublisherProvider; ExternalDataProvider sherpaJournalIssnProvider; + private static final MetadataFieldRef TITLE_FIELD = new MetadataFieldRef("dc", "title", null); + private static final MetadataFieldRef ISSN_FIELD = new MetadataFieldRef("creativeworkseries", "issn", null); + private static final MetadataFieldRef SHERPA_PUBLISHER_FIELD = + new MetadataFieldRef("dc", "identifier", "sherpaPublisher"); + private static final MetadataFieldRef OTHER_FIELD = new MetadataFieldRef("dc", "identifier", "other"); + @BeforeClass public static void setUpClass() { } @@ -83,11 +89,9 @@ public void testGetJournalISSNExternalObject() { String title = null; String identifier = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("creativeworkseries") - && metadataValue.getElement().equalsIgnoreCase("issn")) { + } else if (ISSN_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); } } @@ -131,11 +135,9 @@ public void testSearchJournalISSNExternalObjects() { String title = null; String identifier = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("creativeworkseries") - && metadataValue.getElement().equalsIgnoreCase("issn")) { + } else if (ISSN_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); } } @@ -171,12 +173,9 @@ public void testGetJournalExternalObject() { String title = null; String identifier = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("issn")) { + } else if (ISSN_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); } } @@ -221,12 +220,9 @@ public void testSearchJournalObjects() { String title = null; String identifier = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("issn")) { + } else if (ISSN_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); } } @@ -267,16 +263,11 @@ public void testGetPublisherExternalObject() { String identifier = null; String url = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("sherpaPublisher")) { + } else if (SHERPA_PUBLISHER_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("other")) { + } else if (OTHER_FIELD.matches(metadataValue)) { url = metadataValue.getValue(); } } @@ -327,16 +318,11 @@ public void testSearchPublisherExternalObjects() { String identifier = null; String url = null; for (MetadataValueDTO metadataValue : dataObject.getMetadata()) { - if (metadataValue.getSchema().equalsIgnoreCase("dc") && - metadataValue.getElement().equalsIgnoreCase("title")) { + if (TITLE_FIELD.matches(metadataValue)) { title = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("sherpaPublisher")) { + } else if (SHERPA_PUBLISHER_FIELD.matches(metadataValue)) { identifier = metadataValue.getValue(); - } else if (metadataValue.getSchema().equalsIgnoreCase("dc") - && metadataValue.getElement().equalsIgnoreCase("identifier") - && metadataValue.getQualifier().equalsIgnoreCase("other")) { + } else if (OTHER_FIELD.matches(metadataValue)) { url = metadataValue.getValue(); } } @@ -350,4 +336,27 @@ public void testSearchPublisherExternalObjects() { // Does dc.identifier.other match the expected value? assertEquals("Publisher URL must equal " + validUrl, validUrl, url); } + + private static class MetadataFieldRef { + public final String schema; + public final String element; + public final String qualifier; + + public MetadataFieldRef(String schema, String element, String qualifier) { + this.schema = schema; + this.element = element; + this.qualifier = qualifier; + } + + public boolean matches(MetadataValueDTO value) { + return schema.equalsIgnoreCase(value.getSchema()) && + element.equalsIgnoreCase(value.getElement()) && + (qualifier == null ? value.getQualifier() == null + : qualifier.equalsIgnoreCase(value.getQualifier())); + } + + public MetadataValueDTO toMetadata(String value) { + return new MetadataValueDTO(schema, element, qualifier, null, value); + } + } } \ No newline at end of file From a26ef22a460bc417a6b05f40a2ba3e84f9993c10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 22:58:31 +0000 Subject: [PATCH 092/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.782 to 1.12.783 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.782 to 1.12.783. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.782...1.12.783) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-version: 1.12.783 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index bb246c20ad8f..df1549a98b6b 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -756,7 +756,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.782 + 1.12.783 From f5e83433c4402ec66170c01e7f87bf2f3b9598e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 23:02:34 +0000 Subject: [PATCH 093/701] Bump tika.version from 2.9.3 to 2.9.4 Bumps `tika.version` from 2.9.3 to 2.9.4. Updates `org.apache.tika:tika-core` from 2.9.3 to 2.9.4 - [Changelog](https://github.com/apache/tika/blob/2.9.4/CHANGES.txt) - [Commits](https://github.com/apache/tika/compare/2.9.3...2.9.4) Updates `org.apache.tika:tika-parsers-standard-package` from 2.9.3 to 2.9.4 --- updated-dependencies: - dependency-name: org.apache.tika:tika-core dependency-version: 2.9.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.tika:tika-parsers-standard-package dependency-version: 2.9.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e2683a5844b7..107318fccb15 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ 2.0.34 1.19.0 1.7.36 - 2.9.3 + 2.9.4 1.80 From 8b089be727a2f3888c0b9c3f84727f5cb8d01675 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 23:11:48 +0000 Subject: [PATCH 094/701] Bump com.opencsv:opencsv from 5.10 to 5.11 Bumps com.opencsv:opencsv from 5.10 to 5.11. --- updated-dependencies: - dependency-name: com.opencsv:opencsv dependency-version: '5.11' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index bb246c20ad8f..5e3e67d704cc 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -805,7 +805,7 @@ com.opencsv opencsv - 5.10 + 5.11 From 930565effff8b46cfda9e8b3906ecbbee32227f7 Mon Sep 17 00:00:00 2001 From: abhinav Date: Wed, 19 Mar 2025 15:50:43 +0100 Subject: [PATCH 095/701] 129614: Fixed tests failing in TikaTextExtractionFilterTest when textextractor.use-temp-file is set to true (cherry picked from commit f9f29f49cb804f55ccc58d7a8558777f20061ec5) --- .../org/dspace/app/mediafilter/TikaTextExtractionFilter.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java index e83bf706ed02..b7a6063165b7 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java @@ -138,7 +138,7 @@ private InputStream extractUsingTempFile(InputStream source, boolean verbose) @Override public void characters(char[] ch, int start, int length) throws SAXException { try { - writer.append(new String(ch), start, length); + writer.append(new String(ch, start, length)); } catch (IOException e) { String errorMsg = String.format("Could not append to temporary file at %s " + "when performing text extraction", @@ -156,7 +156,7 @@ public void characters(char[] ch, int start, int length) throws SAXException { @Override public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { try { - writer.append(new String(ch), start, length); + writer.append(new String(ch, start, length)); } catch (IOException e) { String errorMsg = String.format("Could not append to temporary file at %s " + "when performing text extraction", From c2c41e65f86dab0a9d4f4128e16336fcd7b9c215 Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Wed, 16 Apr 2025 14:03:01 +0200 Subject: [PATCH 096/701] [DURACOM-109] Configured proxy settings for all clients --- .../app/client/DSpaceHttpClientFactory.java | 152 +++++++++++ .../app/client/DSpaceProxyRoutePlanner.java | 73 +++++ .../org/dspace/app/sherpa/SHERPAService.java | 8 +- .../dspace/app/util/WebAppServiceImpl.java | 4 +- .../oidc/impl/OidcClientImpl.java | 4 +- .../ctask/general/MetadataWebService.java | 4 +- .../ctask/general/MicrosoftTranslator.java | 4 +- .../dspace/eperson/CaptchaServiceImpl.java | 4 +- .../external/OpenAIRERestConnector.java | 6 +- .../dspace/external/OrcidRestConnector.java | 4 +- .../CCLicenseConnectorServiceImpl.java | 9 +- .../dspace/orcid/client/OrcidClientImpl.java | 15 +- .../impl/HttpConnectionPoolService.java | 4 +- .../statistics/SolrLoggerServiceImpl.java | 6 +- .../export/service/OpenUrlServiceImpl.java | 6 +- .../client/DSpaceHttpClientFactoryTest.java | 256 ++++++++++++++++++ dspace/config/spring/api/core-services.xml | 3 + 17 files changed, 519 insertions(+), 43 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java create mode 100644 dspace-api/src/main/java/org/dspace/app/client/DSpaceProxyRoutePlanner.java create mode 100644 dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java diff --git a/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java b/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java new file mode 100644 index 000000000000..999f13c1b387 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java @@ -0,0 +1,152 @@ +/** + * 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.app.client; + +import static org.apache.commons.collections4.ListUtils.emptyIfNull; + +import java.util.List; + +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.client.HttpClient; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Factory of {@link HttpClient} with common configurations. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class DSpaceHttpClientFactory { + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private DSpaceProxyRoutePlanner proxyRoutePlanner; + + @Autowired(required = false) + private List requestInterceptors; + + @Autowired(required = false) + private List responseInterceptors; + + /** + * Get an instance of {@link DSpaceHttpClientFactory} from the Spring context. + * @return the bean instance + */ + public static DSpaceHttpClientFactory getInstance() { + return new DSpace().getSingletonService(DSpaceHttpClientFactory.class); + } + + /** + * Build an instance of {@link HttpClient} setting the proxy if configured. + * + * @return the client + */ + public CloseableHttpClient build() { + return build(HttpClientBuilder.create(), true); + } + + /** + * return a Builder if an instance of {@link HttpClient} pre-setting the proxy if configured. + * + * @return the client + */ + public HttpClientBuilder builder(boolean setProxy) { + HttpClientBuilder clientBuilder = HttpClientBuilder.create(); + if (setProxy) { + clientBuilder.setRoutePlanner(proxyRoutePlanner); + } + getRequestInterceptors().forEach(clientBuilder::addInterceptorLast); + getResponseInterceptors().forEach(clientBuilder::addInterceptorLast); + return clientBuilder; + } + + /** + * Build an instance of {@link HttpClient} without setting the proxy, even if + * configured. + * + * @return the client + */ + public CloseableHttpClient buildWithoutProxy() { + return build(HttpClientBuilder.create(), false); + } + + /** + * Build an instance of {@link HttpClient} setting the proxy if configured, + * disabling automatic retries and setting the maximum total connection. + * + * @param maxConnTotal the maximum total connection value + * @return the client + */ + public CloseableHttpClient buildWithoutAutomaticRetries(int maxConnTotal) { + HttpClientBuilder clientBuilder = HttpClientBuilder.create() + .disableAutomaticRetries() + .setMaxConnTotal(5); + return build(clientBuilder, true); + } + + /** + * Build an instance of {@link HttpClient} setting the proxy if configured with + * the given request configuration. + * @param requestConfig the request configuration + * @return the client + */ + public CloseableHttpClient buildWithRequestConfig(RequestConfig requestConfig) { + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig); + return build(httpClientBuilder, true); + } + + private CloseableHttpClient build(HttpClientBuilder clientBuilder, boolean setProxy) { + if (setProxy) { + clientBuilder.setRoutePlanner(proxyRoutePlanner); + } + getRequestInterceptors().forEach(clientBuilder::addInterceptorLast); + getResponseInterceptors().forEach(clientBuilder::addInterceptorLast); + return clientBuilder.build(); + } + + public ConfigurationService getConfigurationService() { + return configurationService; + } + + public void setConfigurationService(ConfigurationService configurationService) { + this.configurationService = configurationService; + } + + public List getRequestInterceptors() { + return emptyIfNull(requestInterceptors); + } + + public void setRequestInterceptors(List requestInterceptors) { + this.requestInterceptors = requestInterceptors; + } + + public List getResponseInterceptors() { + return emptyIfNull(responseInterceptors); + } + + public void setResponseInterceptors(List responseInterceptors) { + this.responseInterceptors = responseInterceptors; + } + + public DSpaceProxyRoutePlanner getProxyRoutePlanner() { + return proxyRoutePlanner; + } + + public void setProxyRoutePlanner(DSpaceProxyRoutePlanner proxyRoutePlanner) { + this.proxyRoutePlanner = proxyRoutePlanner; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/client/DSpaceProxyRoutePlanner.java b/dspace-api/src/main/java/org/dspace/app/client/DSpaceProxyRoutePlanner.java new file mode 100644 index 000000000000..1df7fa4a2985 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/client/DSpaceProxyRoutePlanner.java @@ -0,0 +1,73 @@ +/** + * 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.app.client; + +import java.util.Arrays; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.impl.conn.DefaultRoutePlanner; +import org.apache.http.protocol.HttpContext; +import org.dspace.services.ConfigurationService; + +/** + * Extension of {@link DefaultRoutePlanner} that determine the proxy based on + * the configuration service, ignoring configured hosts. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class DSpaceProxyRoutePlanner extends DefaultRoutePlanner { + + private ConfigurationService configurationService; + + public DSpaceProxyRoutePlanner(ConfigurationService configurationService) { + super(null); + this.configurationService = configurationService; + } + + @Override + protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException { + if (isTargetHostConfiguredToBeIgnored(target)) { + return null; + } + String proxyHost = configurationService.getProperty("http.proxy.host"); + String proxyPort = configurationService.getProperty("http.proxy.port"); + if (StringUtils.isAnyBlank(proxyHost, proxyPort)) { + return null; + } + try { + return new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http"); + } catch (NumberFormatException e) { + throw new RuntimeException("Invalid proxy port configuration: " + proxyPort); + } + } + + private boolean isTargetHostConfiguredToBeIgnored(HttpHost target) { + String[] hostsToIgnore = configurationService.getArrayProperty("http.proxy.hosts-to-ignore"); + if (ArrayUtils.isEmpty(hostsToIgnore)) { + return false; + } + return Arrays.stream(hostsToIgnore) + .anyMatch(host -> matchesHost(host, target.getHostName())); + } + + private boolean matchesHost(String hostPattern, String hostName) { + if (hostName.equals(hostPattern)) { + return true; + } else if (hostPattern.startsWith("*")) { + return hostName.endsWith(StringUtils.removeStart(hostPattern, "*")); + } else if (hostPattern.endsWith("*")) { + return hostName.startsWith(StringUtils.removeEnd(hostPattern, "*")); + } + return false; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java index ead725e842c4..73ec0cc58937 100644 --- a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java +++ b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java @@ -23,9 +23,9 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.app.sherpa.v2.SHERPAPublisherResponse; import org.dspace.app.sherpa.v2.SHERPAResponse; import org.dspace.app.sherpa.v2.SHERPAUtils; @@ -63,13 +63,9 @@ public class SHERPAService { * Create a new HTTP builder with sensible defaults in constructor */ public SHERPAService() { - HttpClientBuilder builder = HttpClientBuilder.create(); // httpclient 4.3+ doesn't appear to have any sensible defaults any more. Setting conservative defaults as // not to hammer the SHERPA service too much. - client = builder - .disableAutomaticRetries() - .setMaxConnTotal(5) - .build(); + client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5); } /** diff --git a/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java index 8dcd78c8823b..064a9fdc3e9b 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java @@ -17,8 +17,8 @@ import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpHead; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.app.util.dao.WebAppDAO; import org.dspace.app.util.service.WebAppService; import org.dspace.core.Context; @@ -77,7 +77,7 @@ public List getApps() { for (WebApp app : webApps) { method = new HttpHead(app.getUrl()); int status; - try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { HttpResponse response = client.execute(method); status = response.getStatusLine().getStatusCode(); } diff --git a/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java index ddab01e8cb5d..35856d3756d9 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java @@ -26,8 +26,8 @@ import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authenticate.oidc.OidcClient; import org.dspace.authenticate.oidc.OidcClientException; import org.dspace.authenticate.oidc.model.OidcTokenResponseDTO; @@ -84,7 +84,7 @@ public Map getUserInfo(String accessToken) throws OidcClientExce private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) { - HttpClient client = HttpClientBuilder.create().build(); + HttpClient client = DSpaceHttpClientFactory.getInstance().build(); return executeAndReturns(() -> { diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java index 5891fa017cb0..da7588be6c6f 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java @@ -34,9 +34,9 @@ import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -257,7 +257,7 @@ public int perform(DSpaceObject dso) throws IOException { protected int callService(String value, Item item, StringBuilder resultSb) throws IOException { String callUrl = urlTemplate.replaceAll("\\{" + templateParam + "\\}", value); - CloseableHttpClient client = HttpClientBuilder.create().build(); + CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build(); HttpGet req = new HttpGet(callUrl); for (Map.Entry entry : headers.entrySet()) { req.addHeader(entry.getKey(), entry.getValue()); diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java b/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java index 49c0c36a5917..0d682d9406f4 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java @@ -15,9 +15,9 @@ import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -60,7 +60,7 @@ protected String translateText(String from, String to, String text) throws IOExc String url = baseUrl + "?appId=" + apiKey; url += "&to=" + to + "&from=" + from + "&text=" + text; - try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { HttpGet hm = new HttpGet(url); HttpResponse httpResponse = client.execute(hm); log.debug("Response code from API call is " + httpResponse); diff --git a/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java index 0ab66aea5c2e..e10de57e47a6 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java @@ -22,10 +22,10 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.eperson.service.CaptchaService; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -82,7 +82,7 @@ public void processResponse(String response, String action) throws InvalidReCapt throw new RuntimeException(e.getMessage(), e); } - HttpClient httpClient = HttpClientBuilder.create().build(); + HttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); HttpResponse httpResponse; GoogleCaptchaResponse googleResponse; final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java b/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java index b0aa4aba13a9..06a4b8361436 100644 --- a/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java @@ -32,9 +32,9 @@ import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.app.util.Util; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; @@ -121,7 +121,7 @@ public OpenAIRERestToken grabNewAccessToken() throws IOException { params.add(new BasicNameValuePair("grant_type", "client_credentials")); httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")); - HttpClient httpClient = HttpClientBuilder.create().build(); + HttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); HttpResponse getResponse = httpClient.execute(httpPost); JSONObject responseObject = null; @@ -172,7 +172,7 @@ public InputStream get(String file, String accessToken) { httpGet.addHeader("Authorization", "Bearer " + accessToken); } - HttpClient httpClient = HttpClientBuilder.create().build(); + HttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); getResponse = httpClient.execute(httpGet); StatusLine status = getResponse.getStatusLine(); diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index d45be7e6b56e..3ccbd257e285 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -15,9 +15,9 @@ import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; /** * @author Antoine Snyers (antoine at atmire.com) @@ -50,7 +50,7 @@ public InputStream get(String path, String accessToken) { httpGet.addHeader("Authorization","Bearer " + accessToken); } try { - HttpClient httpClient = HttpClientBuilder.create().build(); + HttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); getResponse = httpClient.execute(httpGet); //do not close this httpClient result = getResponse.getEntity().getContent(); diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index cdecadba5242..524ff04facec 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -28,9 +28,9 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.services.ConfigurationService; import org.jdom2.Attribute; import org.jdom2.Document; @@ -70,12 +70,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, @Override public void afterPropertiesSet() throws Exception { - HttpClientBuilder builder = HttpClientBuilder.create(); - - client = builder - .disableAutomaticRetries() - .setMaxConnTotal(5) - .build(); + client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5); // disallow DTD parsing to ensure no XXE attacks can occur. // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java index 1532abe63412..2285bb11c784 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java @@ -40,8 +40,8 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.orcid.OrcidToken; import org.dspace.orcid.exception.OrcidClientException; import org.dspace.orcid.model.OrcidEntityType; @@ -76,9 +76,12 @@ public class OrcidClientImpl implements OrcidClient { private final ObjectMapper objectMapper; - public OrcidClientImpl(OrcidConfiguration orcidConfiguration) { + private final DSpaceHttpClientFactory httpClientFactory; + + public OrcidClientImpl(OrcidConfiguration orcidConfiguration, DSpaceHttpClientFactory httpClientFactory) { this.orcidConfiguration = orcidConfiguration; this.objectMapper = new ObjectMapper(); + this.httpClientFactory = httpClientFactory; } private static Map, String> initializePathsMap() { @@ -255,7 +258,7 @@ private HttpUriRequest buildDeleteUriRequest(String accessToken, String relative private void executeSuccessful(HttpUriRequest httpUriRequest) { try { - HttpClient client = HttpClientBuilder.create().build(); + HttpClient client = httpClientFactory.build(); HttpResponse response = client.execute(httpUriRequest); if (isNotSuccessfull(response)) { @@ -273,7 +276,7 @@ private void executeSuccessful(HttpUriRequest httpUriRequest) { private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) { - HttpClient client = HttpClientBuilder.create().build(); + HttpClient client = httpClientFactory.build(); return executeAndReturns(() -> { @@ -302,7 +305,7 @@ private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) */ private T executeAndUnmarshall(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull, Class clazz) { - HttpClient client = HttpClientBuilder.create().build(); + HttpClient client = httpClientFactory.build(); return executeAndReturns(() -> { @@ -322,7 +325,7 @@ private T executeAndUnmarshall(HttpUriRequest httpUriRequest, boolean handle } private OrcidResponse execute(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull) { - HttpClient client = HttpClientBuilder.create().build(); + HttpClient client = httpClientFactory.build(); return executeAndReturns(() -> { diff --git a/dspace-api/src/main/java/org/dspace/service/impl/HttpConnectionPoolService.java b/dspace-api/src/main/java/org/dspace/service/impl/HttpConnectionPoolService.java index c5f7c46b586e..27f2ad3e412b 100644 --- a/dspace-api/src/main/java/org/dspace/service/impl/HttpConnectionPoolService.java +++ b/dspace-api/src/main/java/org/dspace/service/impl/HttpConnectionPoolService.java @@ -19,11 +19,11 @@ import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeaderElementIterator; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.services.ConfigurationService; /** @@ -112,7 +112,7 @@ protected void init() { * @return the client. */ public CloseableHttpClient getClient() { - CloseableHttpClient httpClient = HttpClientBuilder.create() + CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().builder(false).create() .setKeepAliveStrategy(keepAliveStrategy) .setConnectionManager(connManager) .build(); diff --git a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java index 5f976bbfd94b..da9c7eb3bf27 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java @@ -51,7 +51,6 @@ import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.solr.client.solrj.SolrClient; @@ -81,6 +80,7 @@ import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.params.ShardParams; import org.apache.solr.common.util.NamedList; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; @@ -1303,7 +1303,7 @@ public void shardSolrIndex() throws IOException, SolrServerException { + "." + i + ".csv"); - try ( CloseableHttpClient hc = HttpClientBuilder.create().build(); ) { + try (CloseableHttpClient hc = DSpaceHttpClientFactory.getInstance().build()) { HttpResponse response = hc.execute(get); csvInputstream = response.getEntity().getContent(); //Write the csv ouput to a file ! @@ -1445,7 +1445,7 @@ public void reindexBitstreamHits(boolean removeDeletedBitstreams) throws Excepti HttpGet get = new HttpGet(solrRequestUrl); List rows; - try ( CloseableHttpClient hc = HttpClientBuilder.create().build(); ) { + try (CloseableHttpClient hc = DSpaceHttpClientFactory.getInstance().build()) { HttpResponse response = hc.execute(get); InputStream csvOutput = response.getEntity().getContent(); Reader csvReader = new InputStreamReader(csvOutput); diff --git a/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java index b7a9562fb541..d8f4aa6b7e7b 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java @@ -18,9 +18,9 @@ import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.core.Context; import org.dspace.statistics.export.OpenURLTracker; import org.springframework.beans.factory.annotation.Autowired; @@ -75,9 +75,7 @@ protected int getResponseCodeFromUrl(final String urlStr) throws IOException { } protected HttpClient getHttpClient(RequestConfig requestConfig) { - return HttpClientBuilder.create() - .setDefaultRequestConfig(requestConfig) - .build(); + return DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(requestConfig); } protected RequestConfig getHttpClientRequestConfig() { diff --git a/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java b/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java new file mode 100644 index 000000000000..b518f19ff4d3 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java @@ -0,0 +1,256 @@ +/** + * 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.app.client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.protocol.HttpContext; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Unit tests for {@link DSpaceHttpClientFactory}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@RunWith(MockitoJUnitRunner.class) +public class DSpaceHttpClientFactoryTest { + + @InjectMocks + private DSpaceHttpClientFactory httpClientFactory; + + @Mock + private ConfigurationService configurationService; + + private MockWebServer mockProxy; + + private MockWebServer mockServer; + + @Before + public void init() { + this.httpClientFactory.setProxyRoutePlanner(new DSpaceProxyRoutePlanner(configurationService)); + this.mockProxy = new MockWebServer(); + this.mockProxy.enqueue(new MockResponse().setResponseCode(200).addHeader("From", "Proxy")); + this.mockServer = new MockWebServer(); + this.mockServer.enqueue(new MockResponse().setResponseCode(200).addHeader("From", "Server")); + } + + @Test + public void testBuildWithProxyConfigured() throws Exception { + setHttpProxyOnConfigurationService(); + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Proxy")); + assertThat(mockProxy.getRequestCount(), is(1)); + assertThat(mockServer.getRequestCount(), is(0)); + RecordedRequest request = mockProxy.takeRequest(100, TimeUnit.MILLISECONDS); + assertThat(request, notNullValue()); + assertThat(request.getRequestUrl(), is(mockProxy.url(""))); + assertThat(request.getRequestLine(), is("GET " + mockServer.url("").toString() + " HTTP/1.1")); + verify(configurationService).getProperty("http.proxy.host"); + verify(configurationService).getProperty("http.proxy.port"); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithProxyConfiguredAndHostToIgnoreSet() throws Exception { + setHttpProxyOnConfigurationService(mockServer.getHostName()); + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithProxyConfiguredAndHostPrefixToIgnoreSet() throws Exception { + setHttpProxyOnConfigurationService("local*", "www.test.com"); + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithProxyConfiguredAndHostSuffixToIgnoreSet() throws Exception { + setHttpProxyOnConfigurationService("www.test.com", "*host"); + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithoutConfiguredProxy() throws Exception { + CloseableHttpClient httpClient = httpClientFactory.build(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + RecordedRequest request = mockServer.takeRequest(100, TimeUnit.MILLISECONDS); + assertThat(request, notNullValue()); + assertThat(request.getRequestUrl(), is(mockServer.url(""))); + assertThat(request.getRequestLine(), is("GET / HTTP/1.1")); + verify(configurationService).getProperty("http.proxy.host"); + verify(configurationService).getProperty("http.proxy.port"); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithoutProxy() throws Exception { + CloseableHttpClient httpClient = httpClientFactory.buildWithoutProxy(); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(0)); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockServer.getRequestCount(), is(1)); + assertThat(mockProxy.getRequestCount(), is(0)); + RecordedRequest request = mockServer.takeRequest(100, TimeUnit.MILLISECONDS); + assertThat(request, notNullValue()); + assertThat(request.getRequestUrl(), is(mockServer.url(""))); + assertThat(request.getRequestLine(), is("GET / HTTP/1.1")); + verifyNoInteractions(configurationService); + } + + @Test + public void testBuildWithoutAutomaticRetries() throws Exception { + setHttpProxyOnConfigurationService("www.test.com"); + CloseableHttpClient httpClient = httpClientFactory.buildWithoutAutomaticRetries(10); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockProxy.getRequestCount(), is(1)); + assertThat(mockServer.getRequestCount(), is(0)); + RecordedRequest request = mockProxy.takeRequest(100, TimeUnit.MILLISECONDS); + assertThat(request, notNullValue()); + assertThat(request.getRequestUrl(), is(mockProxy.url(""))); + assertThat(request.getRequestLine(), is("GET " + mockServer.url("").toString() + " HTTP/1.1")); + verify(configurationService).getProperty("http.proxy.host"); + verify(configurationService).getProperty("http.proxy.port"); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + @Test + public void testBuildWithHttpRequestInterceptor() throws Exception { + setHttpProxyOnConfigurationService("*test.com", "www.dspace.com"); + AtomicReference contextReference = new AtomicReference(); + HttpRequestInterceptor interceptor = (request, context) -> contextReference.set(context); + httpClientFactory.setRequestInterceptors(List.of(interceptor)); + CloseableHttpClient httpClient = httpClientFactory.build(); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockProxy.getRequestCount(), is(1)); + assertThat(mockServer.getRequestCount(), is(0)); + HttpContext httpContext = contextReference.get(); + assertThat(httpContext, notNullValue()); + Object httpRouteObj = httpContext.getAttribute("http.route"); + assertThat(httpRouteObj, notNullValue()); + assertThat(httpRouteObj, instanceOf(HttpRoute.class)); + HttpRoute httpRoute = (HttpRoute) httpRouteObj; + assertThat(httpRoute.getHopCount(), is(2)); + assertThat(httpRoute.getHopTarget(0).getPort(), is(mockProxy.getPort())); + assertThat(httpRoute.getHopTarget(1).getPort(), is(mockServer.getPort())); + } + + @Test + public void testBuildWithHttpResponseInterceptor() throws Exception { + AtomicReference responseReference = new AtomicReference(); + HttpResponseInterceptor responseInterceptor = (response, context) -> responseReference.set(response); + httpClientFactory.setResponseInterceptors(List.of(responseInterceptor)); + CloseableHttpClient httpClient = httpClientFactory.build(); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockProxy.getRequestCount(), is(0)); + assertThat(mockServer.getRequestCount(), is(1)); + HttpResponse httpResponse = responseReference.get(); + assertThat(httpResponse, notNullValue()); + assertThat(httpResponse.getHeaders("From"), arrayWithSize(1)); + assertThat(httpResponse.getHeaders("From")[0].getValue(), is("Server")); + } + + @Test + public void testBuildWithRequestConfig() throws Exception { + setHttpProxyOnConfigurationService(); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(2500) + .build(); + AtomicReference contextReference = new AtomicReference(); + HttpRequestInterceptor interceptor = (request, context) -> contextReference.set(context); + httpClientFactory.setRequestInterceptors(List.of(interceptor)); + CloseableHttpClient httpClient = httpClientFactory.buildWithRequestConfig(requestConfig); + httpClient.execute(new HttpGet(mockServer.url("").toString())); + assertThat(mockProxy.getRequestCount(), is(1)); + assertThat(mockServer.getRequestCount(), is(0)); + HttpContext httpContext = contextReference.get(); + assertThat(httpContext, notNullValue()); + Object httpRequestConfigObj = httpContext.getAttribute("http.request-config"); + assertThat(httpRequestConfigObj, notNullValue()); + assertThat(httpRequestConfigObj, instanceOf(RequestConfig.class)); + assertThat(((RequestConfig) httpRequestConfigObj).getConnectTimeout(), is(2500)); + verify(configurationService).getProperty("http.proxy.host"); + verify(configurationService).getProperty("http.proxy.port"); + verify(configurationService).getArrayProperty("http.proxy.hosts-to-ignore"); + verifyNoMoreInteractions(configurationService); + } + + private void setHttpProxyOnConfigurationService(String... hostsToIgnore) { + when(configurationService.getProperty("http.proxy.host")).thenReturn(mockProxy.getHostName()); + when(configurationService.getProperty("http.proxy.port")).thenReturn(String.valueOf(mockProxy.getPort())); + when(configurationService.getArrayProperty("http.proxy.hosts-to-ignore")).thenReturn(hostsToIgnore); + } +} \ No newline at end of file diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 7a5642f46439..be0e5977ede4 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -153,6 +153,9 @@ + + + From a8d33d3ad06310a5a49865daf59d4f1511bfdf7a Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Thu, 17 Apr 2025 17:55:05 +0200 Subject: [PATCH 097/701] [DURACOM-109] Continued configuring proxy for other classes --- .../app/client/DSpaceHttpClientFactory.java | 2 +- .../packager/AbstractMETSIngester.java | 12 ++++--- .../ctask/general/BasicLinkChecker.java | 18 ++++++---- .../dspace/iiif/IIIFApiQueryServiceImpl.java | 15 ++++---- .../service/LiveImportClientImpl.java | 35 ++++--------------- .../CCLicenseConnectorServiceImpl.java | 18 ++-------- .../statistics/SolrLoggerServiceImpl.java | 4 +-- dspace/config/dspace.cfg | 2 ++ 8 files changed, 39 insertions(+), 67 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java b/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java index 999f13c1b387..59c8172f722c 100644 --- a/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java +++ b/dspace-api/src/main/java/org/dspace/app/client/DSpaceHttpClientFactory.java @@ -93,7 +93,7 @@ public CloseableHttpClient buildWithoutProxy() { public CloseableHttpClient buildWithoutAutomaticRetries(int maxConnTotal) { HttpClientBuilder clientBuilder = HttpClientBuilder.create() .disableAutomaticRetries() - .setMaxConnTotal(5); + .setMaxConnTotal(maxConnTotal); return build(clientBuilder, true); } diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index 0ed0abe21825..c43d8e797b07 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -11,8 +11,6 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; import java.sql.SQLException; import java.util.Iterator; import java.util.List; @@ -21,7 +19,11 @@ import java.util.zip.ZipFile; import org.apache.commons.collections4.CollectionUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; @@ -1312,11 +1314,11 @@ protected static InputStream getFileInputStream(File pkgFile, // we will assume all external files are available via URLs. try { // attempt to open a connection to given URL - URL fileURL = new URL(path); - URLConnection connection = fileURL.openConnection(); + CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(path)); // open stream to access file contents - return connection.getInputStream(); + return httpResponse.getEntity().getContent(); } catch (IOException io) { log .error("Unable to retrieve external file from URL '" diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java index a302159ea9a4..fd6d69c98f04 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java @@ -9,11 +9,15 @@ import java.io.IOException; import java.net.HttpURLConnection; -import java.net.URL; import java.util.ArrayList; import java.util.List; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.MetadataValue; @@ -136,15 +140,15 @@ protected boolean checkURL(String url, StringBuilder results) { */ protected int getResponseStatus(String url, int redirects) { try { - URL theURL = new URL(url); - HttpURLConnection connection = (HttpURLConnection) theURL.openConnection(); - connection.setInstanceFollowRedirects(true); - int statusCode = connection.getResponseCode(); + RequestConfig config = RequestConfig.custom().setRedirectsEnabled(true).build(); + CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(url)); + int statusCode = httpResponse.getStatusLine().getStatusCode(); int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0); if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM || statusCode == HttpURLConnection.HTTP_SEE_OTHER)) { - connection.disconnect(); - String newUrl = connection.getHeaderField("Location"); + httpClient.close(); + String newUrl = httpResponse.getFirstHeader("Location").getValue(); if (newUrl != null && (maxRedirect >= redirects || maxRedirect == -1)) { redirects++; return getResponseStatus(newUrl, redirects); diff --git a/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java b/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java index 7c6336ed3c7f..87bd7770648e 100644 --- a/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java @@ -12,12 +12,14 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.content.Bitstream; import org.dspace.iiif.util.IIIFSharedUtils; @@ -35,14 +37,11 @@ public class IIIFApiQueryServiceImpl implements IIIFApiQueryService { public int[] getImageDimensions(Bitstream bitstream) { int[] arr = new int[2]; String path = IIIFSharedUtils.getInfoJsonPath(bitstream); - URL url; BufferedReader in = null; try { - url = new URL(path); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod("GET"); - in = new BufferedReader( - new InputStreamReader(con.getInputStream())); + CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(path)); + in = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent())); String inputLine; StringBuilder response = new StringBuilder(); while ((inputLine = in.readLine()) != null) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java index 1a8a7a7861ed..2cb8236842a3 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java @@ -17,19 +17,16 @@ import org.apache.commons.collections.MapUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.config.RequestConfig.Builder; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.utils.URIBuilder; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -53,16 +50,12 @@ public class LiveImportClientImpl implements LiveImportClient { @Override public String executeHttpGetRequest(int timeout, String URL, Map> params) { HttpGet method = null; + RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(timeout).build(); try (CloseableHttpClient httpClient = Optional.ofNullable(this.httpClient) - .orElseGet(HttpClients::createDefault)) { - - Builder requestConfigBuilder = RequestConfig.custom(); - requestConfigBuilder.setConnectionRequestTimeout(timeout); - RequestConfig defaultRequestConfig = requestConfigBuilder.build(); - + .orElse(DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config))) { String uri = buildUrl(URL, params.get(URI_PARAMETERS)); method = new HttpGet(uri); - method.setConfig(defaultRequestConfig); + method.setConfig(config); Map headerParams = params.get(HEADER_PARAMETERS); if (MapUtils.isNotEmpty(headerParams)) { @@ -71,7 +64,6 @@ public String executeHttpGetRequest(int timeout, String URL, Map> params, String entry) { HttpPost method = null; + RequestConfig config = RequestConfig.custom().build(); try (CloseableHttpClient httpClient = Optional.ofNullable(this.httpClient) - .orElseGet(HttpClients::createDefault)) { - - Builder requestConfigBuilder = RequestConfig.custom(); - RequestConfig defaultRequestConfig = requestConfigBuilder.build(); + .orElse(DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config))) { String uri = buildUrl(URL, params.get(URI_PARAMETERS)); method = new HttpPost(uri); - method.setConfig(defaultRequestConfig); if (StringUtils.isNotBlank(entry)) { method.setEntity(new StringEntity(entry)); } setHeaderParams(method, params); - configureProxy(method, defaultRequestConfig); if (log.isDebugEnabled()) { log.debug("Performing POST request to \"" + uri + "\"..." ); } @@ -129,17 +117,6 @@ public String executeHttpPostRequest(String URL, Map return StringUtils.EMPTY; } - private void configureProxy(HttpRequestBase method, RequestConfig defaultRequestConfig) { - String proxyHost = configurationService.getProperty("http.proxy.host"); - String proxyPort = configurationService.getProperty("http.proxy.port"); - if (StringUtils.isNotBlank(proxyHost) && StringUtils.isNotBlank(proxyPort)) { - RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig) - .setProxy(new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http")) - .build(); - method.setConfig(requestConfig); - } - } - /** * Allows to set the header parameters to the HTTP Post method * diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index 524ff04facec..e9fe49e9b4f6 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -10,9 +10,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringReader; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; import java.text.MessageFormat; import java.util.Collections; import java.util.HashMap; @@ -328,23 +325,14 @@ private String createAnswerString(final Map parameterMap) { @Override public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { String ccLicenseUrl = configurationService.getProperty("cc.api.rooturl"); - String issueUrl = ccLicenseUrl + "/details?license-uri=" + licenseURI; - - URL request_url; - try { - request_url = new URL(issueUrl); - } catch (MalformedURLException e) { - return null; - } - URLConnection connection = request_url.openConnection(); - connection.setDoOutput(true); try { + CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpPost(issueUrl)); // parsing document from input stream - InputStream stream = connection.getInputStream(); + InputStream stream = httpResponse.getEntity().getContent(); Document doc = parser.build(stream); return doc; - } catch (Exception e) { log.error("Error while retrieving the license document for URI: " + licenseURI, e); } diff --git a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java index da9c7eb3bf27..6c3ccdc082eb 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java @@ -1303,7 +1303,7 @@ public void shardSolrIndex() throws IOException, SolrServerException { + "." + i + ".csv"); - try (CloseableHttpClient hc = DSpaceHttpClientFactory.getInstance().build()) { + try (CloseableHttpClient hc = DSpaceHttpClientFactory.getInstance().buildWithoutProxy()) { HttpResponse response = hc.execute(get); csvInputstream = response.getEntity().getContent(); //Write the csv ouput to a file ! @@ -1445,7 +1445,7 @@ public void reindexBitstreamHits(boolean removeDeletedBitstreams) throws Excepti HttpGet get = new HttpGet(solrRequestUrl); List rows; - try (CloseableHttpClient hc = DSpaceHttpClientFactory.getInstance().build()) { + try (CloseableHttpClient hc = DSpaceHttpClientFactory.getInstance().buildWithoutProxy()) { HttpResponse response = hc.execute(get); InputStream csvOutput = response.getEntity().getContent(); Reader csvReader = new InputStreamReader(csvOutput); diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 94b731cd3488..4621ca51dc13 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -411,6 +411,8 @@ http.proxy.host = # port number of proxy server http.proxy.port = +http.proxy.hosts-to-ignore = 127.0.0.1, localhost + # If enabled, the logging and the Solr statistics system will look for an X-Forwarded-For header. # If it finds it, it will use this for the user IP address. # NOTE: This is required to be enabled if you plan to use the Angular UI, as the server-side rendering provided in From 32dd1a3dd22c70363c0c7593d395570247ee00aa Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Fri, 18 Apr 2025 10:24:35 +0200 Subject: [PATCH 098/701] [DURACOM-109] Minor fix --- .../external/liveimportclient/service/LiveImportClientImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java index 2cb8236842a3..84475b62c07b 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java @@ -55,7 +55,6 @@ public String executeHttpGetRequest(int timeout, String URL, Map headerParams = params.get(HEADER_PARAMETERS); if (MapUtils.isNotEmpty(headerParams)) { From b9352c9149b90ae3c5f007ddae15632b26d9e246 Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Fri, 18 Apr 2025 15:00:38 +0200 Subject: [PATCH 099/701] [DURACOM-109] Fixed http connection leaks --- .../org/dspace/app/sherpa/SHERPAService.java | 21 +--- .../dspace/app/util/WebAppServiceImpl.java | 4 +- .../oidc/impl/OidcClientImpl.java | 29 +++-- .../packager/AbstractMETSIngester.java | 3 +- .../ctask/general/BasicLinkChecker.java | 5 +- .../dspace/external/OrcidRestConnector.java | 10 +- .../dspace/iiif/IIIFApiQueryServiceImpl.java | 3 +- .../CCLicenseConnectorServiceImpl.java | 3 +- .../dspace/orcid/client/OrcidClientImpl.java | 103 ++++++++---------- 9 files changed, 73 insertions(+), 108 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java index 73ec0cc58937..87042c502d83 100644 --- a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java +++ b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java @@ -17,9 +17,9 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.CloseableHttpClient; @@ -45,8 +45,6 @@ */ public class SHERPAService { - private CloseableHttpClient client = null; - private int maxNumberOfTries; private long sleepBetweenTimeouts; private int timeout = 5000; @@ -59,15 +57,6 @@ public class SHERPAService { @Autowired ConfigurationService configurationService; - /** - * Create a new HTTP builder with sensible defaults in constructor - */ - public SHERPAService() { - // httpclient 4.3+ doesn't appear to have any sensible defaults any more. Setting conservative defaults as - // not to hammer the SHERPA service too much. - client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5); - } - /** * Complete initialization of the Bean. */ @@ -128,14 +117,14 @@ public SHERPAPublisherResponse performPublisherRequest(String type, String field timeout, sleepBetweenTimeouts)); - try { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { Thread.sleep(sleepBetweenTimeouts); // Construct a default HTTP method (first result) method = constructHttpGet(type, field, predicate, value, start, limit); // Execute the method - HttpResponse response = client.execute(method); + CloseableHttpResponse response = client.execute(method); int statusCode = response.getStatusLine().getStatusCode(); log.debug(response.getStatusLine().getStatusCode() + ": " @@ -231,14 +220,14 @@ public SHERPAResponse performRequest(String type, String field, String predicate timeout, sleepBetweenTimeouts)); - try { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { Thread.sleep(sleepBetweenTimeouts); // Construct a default HTTP method (first result) method = constructHttpGet(type, field, predicate, value, start, limit); // Execute the method - HttpResponse response = client.execute(method); + CloseableHttpResponse response = client.execute(method); int statusCode = response.getStatusLine().getStatusCode(); log.debug(response.getStatusLine().getStatusCode() + ": " diff --git a/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java index 064a9fdc3e9b..fa23903ba431 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/WebAppServiceImpl.java @@ -13,8 +13,8 @@ import java.util.Date; import java.util.List; -import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpHead; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; @@ -78,7 +78,7 @@ public List getApps() { method = new HttpHead(app.getUrl()); int status; try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { - HttpResponse response = client.execute(method); + CloseableHttpResponse response = client.execute(method); status = response.getStatusLine().getStatusCode(); } if (status != HttpStatus.SC_OK) { diff --git a/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java index 35856d3756d9..8dd609951126 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/oidc/impl/OidcClientImpl.java @@ -22,10 +22,11 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authenticate.oidc.OidcClient; @@ -83,21 +84,17 @@ public Map getUserInfo(String accessToken) throws OidcClientExce } private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) { - - HttpClient client = DSpaceHttpClientFactory.getInstance().build(); - - return executeAndReturns(() -> { - - HttpResponse response = client.execute(httpUriRequest); - - if (isNotSuccessfull(response)) { - throw new OidcClientException(getStatusCode(response), formatErrorMessage(response)); - } - - return objectMapper.readValue(getContent(response), clazz); - - }); - + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + return executeAndReturns(() -> { + CloseableHttpResponse response = client.execute(httpUriRequest); + if (isNotSuccessfull(response)) { + throw new OidcClientException(getStatusCode(response), formatErrorMessage(response)); + } + return objectMapper.readValue(getContent(response), clazz); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } } private T executeAndReturns(ThrowingSupplier supplier) { diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index c43d8e797b07..835b9f0e9b6f 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -1312,9 +1312,8 @@ protected static InputStream getFileInputStream(File pkgFile, if (params.getBooleanProperty("manifestOnly", false)) { // NOTE: since we are only dealing with a METS manifest, // we will assume all external files are available via URLs. - try { + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { // attempt to open a connection to given URL - CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(path)); // open stream to access file contents diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java index fd6d69c98f04..189f77733886 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java @@ -139,9 +139,8 @@ protected boolean checkURL(String url, StringBuilder results) { * @return The HTTP response code (e.g. 200 / 301 / 404 / 500) */ protected int getResponseStatus(String url, int redirects) { - try { - RequestConfig config = RequestConfig.custom().setRedirectsEnabled(true).build(); - CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config); + RequestConfig config = RequestConfig.custom().setRedirectsEnabled(true).build(); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config)) { CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(url)); int statusCode = httpResponse.getStatusLine().getStatusCode(); int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0); diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index 3ccbd257e285..f2c6246f4d06 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -12,9 +12,9 @@ import java.util.Scanner; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -39,7 +39,7 @@ public OrcidRestConnector(String url) { } public InputStream get(String path, String accessToken) { - HttpResponse getResponse = null; + CloseableHttpResponse getResponse = null; InputStream result = null; path = trimSlashes(path); @@ -49,10 +49,8 @@ public InputStream get(String path, String accessToken) { httpGet.addHeader("Content-Type", "application/vnd.orcid+xml"); httpGet.addHeader("Authorization","Bearer " + accessToken); } - try { - HttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { getResponse = httpClient.execute(httpGet); - //do not close this httpClient result = getResponse.getEntity().getContent(); } catch (Exception e) { getGotError(e, fullPath); diff --git a/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java b/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java index 87bd7770648e..ccb2c170d949 100644 --- a/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/iiif/IIIFApiQueryServiceImpl.java @@ -38,8 +38,7 @@ public int[] getImageDimensions(Bitstream bitstream) { int[] arr = new int[2]; String path = IIIFSharedUtils.getInfoJsonPath(bitstream); BufferedReader in = null; - try { - CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(path)); in = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent())); String inputLine; diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index e9fe49e9b4f6..1d777a2e13c8 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -326,8 +326,7 @@ private String createAnswerString(final Map parameterMap) { public Document retrieveLicenseRDFDoc(String licenseURI) throws IOException { String ccLicenseUrl = configurationService.getProperty("cc.api.rooturl"); String issueUrl = ccLicenseUrl + "/details?license-uri=" + licenseURI; - try { - CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { CloseableHttpResponse httpResponse = httpClient.execute(new HttpPost(issueUrl)); // parsing document from input stream InputStream stream = httpResponse.getEntity().getContent(); diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java index 2285bb11c784..954336da2573 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java @@ -35,11 +35,12 @@ import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.orcid.OrcidToken; @@ -76,12 +77,9 @@ public class OrcidClientImpl implements OrcidClient { private final ObjectMapper objectMapper; - private final DSpaceHttpClientFactory httpClientFactory; - - public OrcidClientImpl(OrcidConfiguration orcidConfiguration, DSpaceHttpClientFactory httpClientFactory) { + public OrcidClientImpl(OrcidConfiguration orcidConfiguration) { this.orcidConfiguration = orcidConfiguration; this.objectMapper = new ObjectMapper(); - this.httpClientFactory = httpClientFactory; } private static Map, String> initializePathsMap() { @@ -257,10 +255,8 @@ private HttpUriRequest buildDeleteUriRequest(String accessToken, String relative } private void executeSuccessful(HttpUriRequest httpUriRequest) { - try { - HttpClient client = httpClientFactory.build(); - HttpResponse response = client.execute(httpUriRequest); - + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + CloseableHttpResponse response = client.execute(httpUriRequest); if (isNotSuccessfull(response)) { throw new OrcidClientException( getStatusCode(response), @@ -275,21 +271,17 @@ private void executeSuccessful(HttpUriRequest httpUriRequest) { } private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) { - - HttpClient client = httpClientFactory.build(); - - return executeAndReturns(() -> { - - HttpResponse response = client.execute(httpUriRequest); - - if (isNotSuccessfull(response)) { - throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); - } - - return objectMapper.readValue(response.getEntity().getContent(), clazz); - - }); - + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + return executeAndReturns(() -> { + CloseableHttpResponse response = client.execute(httpUriRequest); + if (isNotSuccessfull(response)) { + throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); + } + return objectMapper.readValue(response.getEntity().getContent(), clazz); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } } /** @@ -304,44 +296,37 @@ private T executeAndParseJson(HttpUriRequest httpUriRequest, Class clazz) * @throws OrcidClientException if the incoming response is not successfull */ private T executeAndUnmarshall(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull, Class clazz) { - - HttpClient client = httpClientFactory.build(); - - return executeAndReturns(() -> { - - HttpResponse response = client.execute(httpUriRequest); - - if (handleNotFoundAsNull && isNotFound(response)) { - return null; - } - - if (isNotSuccessfull(response)) { - throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); - } - - return unmarshall(response.getEntity(), clazz); - - }); + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + return executeAndReturns(() -> { + CloseableHttpResponse response = client.execute(httpUriRequest); + if (handleNotFoundAsNull && isNotFound(response)) { + return null; + } + if (isNotSuccessfull(response)) { + throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); + } + return unmarshall(response.getEntity(), clazz); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } } private OrcidResponse execute(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull) { - HttpClient client = httpClientFactory.build(); - - return executeAndReturns(() -> { - - HttpResponse response = client.execute(httpUriRequest); - - if (handleNotFoundAsNull && isNotFound(response)) { - return new OrcidResponse(getStatusCode(response), null, getContent(response)); - } - - if (isNotSuccessfull(response)) { - throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); - } - - return new OrcidResponse(getStatusCode(response), getPutCode(response), getContent(response)); - - }); + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + return executeAndReturns(() -> { + CloseableHttpResponse response = client.execute(httpUriRequest); + if (handleNotFoundAsNull && isNotFound(response)) { + return new OrcidResponse(getStatusCode(response), null, getContent(response)); + } + if (isNotSuccessfull(response)) { + throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response)); + } + return new OrcidResponse(getStatusCode(response), getPutCode(response), getContent(response)); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } } private T executeAndReturns(ThrowingSupplier supplier) { From f9307b617c28575f8b3cfbc3563bf4a2764d902e Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Fri, 18 Apr 2025 15:12:55 +0200 Subject: [PATCH 100/701] [DURACOM-109] Minor fix --- .../java/org/dspace/service/impl/HttpConnectionPoolService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/service/impl/HttpConnectionPoolService.java b/dspace-api/src/main/java/org/dspace/service/impl/HttpConnectionPoolService.java index 27f2ad3e412b..69fea9c72c5f 100644 --- a/dspace-api/src/main/java/org/dspace/service/impl/HttpConnectionPoolService.java +++ b/dspace-api/src/main/java/org/dspace/service/impl/HttpConnectionPoolService.java @@ -112,7 +112,7 @@ protected void init() { * @return the client. */ public CloseableHttpClient getClient() { - CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().builder(false).create() + CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().builder(true).create() .setKeepAliveStrategy(keepAliveStrategy) .setConnectionManager(connManager) .build(); From f4390fef52c9ae352ef2b305668c000d4c84853f Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Wed, 23 Apr 2025 12:31:11 +0200 Subject: [PATCH 101/701] [DURACOM-109] Continued fixing http connection leaks --- .../packager/AbstractMETSIngester.java | 8 +- .../ctask/general/BasicLinkChecker.java | 24 +++--- .../ctask/general/MetadataWebService.java | 85 +++++++++---------- .../ctask/general/MicrosoftTranslator.java | 26 +++--- .../export/service/OpenUrlServiceImpl.java | 16 ++-- .../service/OpenUrlServiceImplTest.java | 10 +-- 6 files changed, 82 insertions(+), 87 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index 835b9f0e9b6f..77236be9d525 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -1314,10 +1314,10 @@ protected static InputStream getFileInputStream(File pkgFile, // we will assume all external files are available via URLs. try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { // attempt to open a connection to given URL - CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(path)); - - // open stream to access file contents - return httpResponse.getEntity().getContent(); + try (CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(path))) { + // open stream to access file contents + return httpResponse.getEntity().getContent(); + } } catch (IOException io) { log .error("Unable to retrieve external file from URL '" diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java index 189f77733886..a6621fcb2b41 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java @@ -141,21 +141,19 @@ protected boolean checkURL(String url, StringBuilder results) { protected int getResponseStatus(String url, int redirects) { RequestConfig config = RequestConfig.custom().setRedirectsEnabled(true).build(); try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config)) { - CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(url)); - int statusCode = httpResponse.getStatusLine().getStatusCode(); - int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0); - if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM || - statusCode == HttpURLConnection.HTTP_SEE_OTHER)) { - httpClient.close(); - String newUrl = httpResponse.getFirstHeader("Location").getValue(); - if (newUrl != null && (maxRedirect >= redirects || maxRedirect == -1)) { - redirects++; - return getResponseStatus(newUrl, redirects); + try (CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(url))) { + int statusCode = httpResponse.getStatusLine().getStatusCode(); + int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0); + if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM || + statusCode == HttpURLConnection.HTTP_SEE_OTHER)) { + String newUrl = httpResponse.getFirstHeader("Location").getValue(); + if (newUrl != null && (maxRedirect >= redirects || maxRedirect == -1)) { + redirects++; + return getResponseStatus(newUrl, redirects); + } } - + return statusCode; } - return statusCode; - } catch (IOException ioe) { // Must be a bad URL log.debug("Bad link: " + ioe.getMessage()); diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java index da7588be6c6f..7f61cad412e2 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java @@ -30,8 +30,8 @@ import javax.xml.xpath.XPathFactory; import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.LogManager; @@ -255,53 +255,50 @@ public int perform(DSpaceObject dso) throws IOException { } protected int callService(String value, Item item, StringBuilder resultSb) throws IOException { - String callUrl = urlTemplate.replaceAll("\\{" + templateParam + "\\}", value); - CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build(); - HttpGet req = new HttpGet(callUrl); - for (Map.Entry entry : headers.entrySet()) { - req.addHeader(entry.getKey(), entry.getValue()); - } - HttpResponse resp = client.execute(req); - int status = Curator.CURATE_ERROR; - int statusCode = resp.getStatusLine().getStatusCode(); - if (statusCode == HttpStatus.SC_OK) { - HttpEntity entity = resp.getEntity(); - if (entity != null) { - // boiler-plate handling taken from Apache 4.1 javadoc - InputStream instream = entity.getContent(); - try { - // This next line triggers a false-positive XXE warning from LGTM, even though we disallow DTD - // parsing during initialization of docBuilder in init() - Document doc = docBuilder.parse(instream); // lgtm [java/xxe] - status = processResponse(doc, item, resultSb); - } catch (SAXException saxE) { - log.error("caught exception: " + saxE); - resultSb.append(" unable to read response document"); - } catch (RuntimeException ex) { - // In case of an unexpected exception you may want to abort - // the HTTP request in order to shut down the underlying - // connection and release it back to the connection manager. - req.abort(); - log.error("caught exception: " + ex); - throw ex; - } finally { - // Closing the input stream will trigger connection release - instream.close(); + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + HttpGet req = new HttpGet(callUrl); + for (Map.Entry entry : headers.entrySet()) { + req.addHeader(entry.getKey(), entry.getValue()); + } + try (CloseableHttpResponse resp = client.execute(req)) { + int status = Curator.CURATE_ERROR; + int statusCode = resp.getStatusLine().getStatusCode(); + if (statusCode == HttpStatus.SC_OK) { + HttpEntity entity = resp.getEntity(); + if (entity != null) { + // boiler-plate handling taken from Apache 4.1 javadoc + InputStream instream = entity.getContent(); + try { + // This next line triggers a false-positive XXE warning from LGTM, even though we disallow DTD + // parsing during initialization of docBuilder in init() + Document doc = docBuilder.parse(instream); // lgtm [java/xxe] + status = processResponse(doc, item, resultSb); + } catch (SAXException saxE) { + log.error("caught exception: " + saxE); + resultSb.append(" unable to read response document"); + } catch (RuntimeException ex) { + // In case of an unexpected exception you may want to abort + // the HTTP request in order to shut down the underlying + // connection and release it back to the connection manager. + req.abort(); + log.error("caught exception: " + ex); + throw ex; + } finally { + // Closing the input stream will trigger connection release + instream.close(); + } + } else { + log.error(" obtained no valid service response"); + resultSb.append("no service response"); + } + } else { + log.error("service returned non-OK status: " + statusCode); + resultSb.append("no service response"); } - // When HttpClient instance is no longer needed, - // shut down the connection manager to ensure - // immediate deallocation of all system resources - client.close(); - } else { - log.error(" obtained no valid service response"); - resultSb.append("no service response"); + return status; } - } else { - log.error("service returned non-OK status: " + statusCode); - resultSb.append("no service response"); } - return status; } protected int processResponse(Document doc, Item item, StringBuilder resultSb) throws IOException { diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java b/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java index 0d682d9406f4..af9c2de0c8b1 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MicrosoftTranslator.java @@ -12,7 +12,7 @@ import java.nio.charset.StandardCharsets; import org.apache.commons.io.IOUtils; -import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.LogManager; @@ -62,20 +62,18 @@ protected String translateText(String from, String to, String text) throws IOExc try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { HttpGet hm = new HttpGet(url); - HttpResponse httpResponse = client.execute(hm); - log.debug("Response code from API call is " + httpResponse); - - if (httpResponse.getStatusLine().getStatusCode() == 200) { - String response = IOUtils.toString(httpResponse.getEntity().getContent(), - StandardCharsets.ISO_8859_1); - response = response - .replaceAll("", ""); - response = response.replaceAll("", ""); - translatedText = response; + try (CloseableHttpResponse httpResponse = client.execute(hm)) { + log.debug("Response code from API call is " + httpResponse); + if (httpResponse.getStatusLine().getStatusCode() == 200) { + String response = IOUtils.toString(httpResponse.getEntity().getContent(), + StandardCharsets.ISO_8859_1); + response = response + .replaceAll("", ""); + response = response.replaceAll("", ""); + translatedText = response; + } } } - return translatedText; } -} - +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java index d8f4aa6b7e7b..64014fcbc07c 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/export/service/OpenUrlServiceImpl.java @@ -14,10 +14,10 @@ import java.util.List; import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -68,13 +68,15 @@ public void processUrl(Context c, String urlStr) throws SQLException { * @throws IOException */ protected int getResponseCodeFromUrl(final String urlStr) throws IOException { - HttpGet httpGet = new HttpGet(urlStr); - HttpClient httpClient = getHttpClient(getHttpClientRequestConfig()); - HttpResponse httpResponse = httpClient.execute(httpGet); - return httpResponse.getStatusLine().getStatusCode(); + try (CloseableHttpClient httpClient = getHttpClient(getHttpClientRequestConfig())) { + HttpGet httpGet = new HttpGet(urlStr); + try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) { + return httpResponse.getStatusLine().getStatusCode(); + } + } } - protected HttpClient getHttpClient(RequestConfig requestConfig) { + protected CloseableHttpClient getHttpClient(RequestConfig requestConfig) { return DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(requestConfig); } diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/service/OpenUrlServiceImplTest.java b/dspace-api/src/test/java/org/dspace/statistics/export/service/OpenUrlServiceImplTest.java index d214050e6b5a..718ef701e136 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/export/service/OpenUrlServiceImplTest.java +++ b/dspace-api/src/test/java/org/dspace/statistics/export/service/OpenUrlServiceImplTest.java @@ -27,9 +27,9 @@ import java.util.Date; import java.util.List; -import org.apache.http.HttpResponse; import org.apache.http.StatusLine; -import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; import org.dspace.core.Context; import org.dspace.statistics.export.OpenURLTracker; import org.junit.Before; @@ -55,7 +55,7 @@ public class OpenUrlServiceImplTest { private FailedOpenURLTrackerService failedOpenURLTrackerService; @Mock - private HttpClient httpClient; + private CloseableHttpClient httpClient; @Before public void setUp() throws Exception { @@ -74,11 +74,11 @@ public void setUp() throws Exception { * @param statusCode the http status code to use in the mock. * @return a mocked http response. */ - protected HttpResponse createMockHttpResponse(int statusCode) { + protected CloseableHttpResponse createMockHttpResponse(int statusCode) { StatusLine statusLine = mock(StatusLine.class); when(statusLine.getStatusCode()).thenReturn(statusCode); - HttpResponse httpResponse = mock(HttpResponse.class); + CloseableHttpResponse httpResponse = mock(CloseableHttpResponse.class); when(httpResponse.getStatusLine()).thenReturn(statusLine); return httpResponse; From 7f865ad95657e9712d6ebec41a765bc9d8b6f2c7 Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Wed, 23 Apr 2025 15:34:52 +0200 Subject: [PATCH 102/701] [DURACOM-109] Linter error fix --- .../ctask/general/BasicLinkChecker.java | 21 +++++++++---------- .../ctask/general/MetadataWebService.java | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java index a6621fcb2b41..020331842703 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java @@ -141,19 +141,18 @@ protected boolean checkURL(String url, StringBuilder results) { protected int getResponseStatus(String url, int redirects) { RequestConfig config = RequestConfig.custom().setRedirectsEnabled(true).build(); try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config)) { - try (CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(url))) { - int statusCode = httpResponse.getStatusLine().getStatusCode(); - int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0); - if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM || - statusCode == HttpURLConnection.HTTP_SEE_OTHER)) { - String newUrl = httpResponse.getFirstHeader("Location").getValue(); - if (newUrl != null && (maxRedirect >= redirects || maxRedirect == -1)) { - redirects++; - return getResponseStatus(newUrl, redirects); - } + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(url)); + int statusCode = httpResponse.getStatusLine().getStatusCode(); + int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0); + if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM || + statusCode == HttpURLConnection.HTTP_SEE_OTHER)) { + String newUrl = httpResponse.getFirstHeader("Location").getValue(); + if (newUrl != null && (maxRedirect >= redirects || maxRedirect == -1)) { + redirects++; + return getResponseStatus(newUrl, redirects); } - return statusCode; } + return statusCode; } catch (IOException ioe) { // Must be a bad URL log.debug("Bad link: " + ioe.getMessage()); diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java index 7f61cad412e2..fc62d7a4b23f 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java @@ -270,8 +270,8 @@ protected int callService(String value, Item item, StringBuilder resultSb) throw // boiler-plate handling taken from Apache 4.1 javadoc InputStream instream = entity.getContent(); try { - // This next line triggers a false-positive XXE warning from LGTM, even though we disallow DTD - // parsing during initialization of docBuilder in init() + // This next line triggers a false-positive XXE warning from LGTM, even though + // we disallow DTD parsing during initialization of docBuilder in init() Document doc = docBuilder.parse(instream); // lgtm [java/xxe] status = processResponse(doc, item, resultSb); } catch (SAXException saxE) { From 0f1679ed7283a799364ac317c8a6fcbbfc290dcf Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Mon, 28 Apr 2025 08:51:06 +0200 Subject: [PATCH 103/701] [DURACOM-109] fix typo and correct logic for ORCID connector --- dspace-api/pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index bb246c20ad8f..247d2da63c16 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -521,10 +521,6 @@ org.apache.pdfbox pdfbox - - org.apache.pdfbox - fontbox - com.ibm.icu icu4j From 9d6c482cc41a4f7923feb5ca03cd11d5552a7d64 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Mon, 28 Apr 2025 10:50:32 +0200 Subject: [PATCH 104/701] [DURACOM-109] Orcid connector fix and improvement --- .../provider/impl/OrcidV3AuthorDataProvider.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index dfbd07a83a02..a9e10f92948d 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -169,13 +169,7 @@ public Person getBio(String id) { } initializeAccessToken(); InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - Person person = converter.convertSinglePerson(bioDocument); - try { - bioDocument.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - return person; + return converter.convertSinglePerson(bioDocument); } /** @@ -220,11 +214,6 @@ public List searchExternalDataObjects(String query, int star } } } - try { - bioDocument.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); } From dbcaac4b0839a922e5e99f9104d617f6b650be8e Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Tue, 29 Apr 2025 08:52:36 +0200 Subject: [PATCH 105/701] [DURACOM-109] added checkstyle rules to forbid usage of HttpClientBuilder.create() --- checkstyle-suppressions.xml | 1 + checkstyle.xml | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 77e27b8768ac..46bd9ca80d62 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -7,4 +7,5 @@ + diff --git a/checkstyle.xml b/checkstyle.xml index a33fc4831950..36d2b15bd89e 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -136,5 +136,22 @@ For more information on CheckStyle configurations below, see: http://checkstyle. + + + + + + + + + + + + + + + + + From 409b775d3531357bf1ed45f02571447d504c2f33 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Tue, 29 Apr 2025 09:07:25 +0200 Subject: [PATCH 106/701] [DURACOM-109] fix TruncatedChunkException error --- .../main/java/org/dspace/external/OrcidRestConnector.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index f2c6246f4d06..aa16af7a524d 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -7,6 +7,7 @@ */ package org.dspace.external; +import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Scanner; @@ -51,7 +52,11 @@ public InputStream get(String path, String accessToken) { } try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { getResponse = httpClient.execute(httpGet); - result = getResponse.getEntity().getContent(); + try (InputStream responseStream = getResponse.getEntity().getContent()) { + // Read all the content of the response stream into a byte array to prevent TruncatedChunkException + byte[] content = responseStream.readAllBytes(); + result = new ByteArrayInputStream(content); + } } catch (Exception e) { getGotError(e, fullPath); } From 032252664b3c7997231fdb8b10f3fe3c4951cdd9 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Wed, 26 Feb 2025 10:53:25 +0100 Subject: [PATCH 107/701] [DURACOM-328] fix error in check for Patch request --- .../dspaceFolder/config/item-submission.xml | 12 +++++ .../config/spring/api/access-conditions.xml | 13 ++++++ .../app/rest/submit/step/UploadStep.java | 18 ++++++-- .../rest/WorkflowItemRestRepositoryIT.java | 46 +++++++++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index 2b4cee044916..0ffac4b49db8 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -26,6 +26,7 @@ + @@ -179,6 +180,11 @@ submission + + submit.progressbar.upload-no-required-metadata + org.dspace.app.rest.submit.step.UploadStep + upload + @@ -267,6 +273,12 @@ + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml index a9af7c66f5e8..bf02e6a23ed2 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml @@ -55,6 +55,7 @@ + @@ -116,4 +117,16 @@ + + + + + + + + + + + + diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java index b91916ca3199..89003549f73c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/UploadStep.java @@ -10,6 +10,7 @@ import java.io.BufferedInputStream; import java.io.InputStream; import java.util.List; +import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import org.apache.logging.log4j.Logger; @@ -48,6 +49,13 @@ public class UploadStep extends AbstractProcessingStep public static final String UPLOAD_STEP_METADATA_SECTION = "bitstream-metadata"; + private static final Pattern UPDATE_METADATA_PATTERN = + Pattern.compile("^/sections/[^/]+/files/[^/]+/metadata/[^/]+(/[^/]+)?$"); + private static final Pattern PRIMARY_FLAG_PATTERN = + Pattern.compile("^/sections/[^/]+/primary$"); + private static final Pattern ACCESS_CONDITION_PATTERN = + Pattern.compile("^/sections/[^/]+/files/[^/]+/accessConditions(/[^/]+)?$"); + @Override public DataUpload getData(SubmissionService submissionService, InProgressSubmission obj, SubmissionStepConfig config) throws Exception { @@ -69,23 +77,23 @@ public void doPatchProcessing(Context context, HttpServletRequest currentRequest String instance = null; if ("remove".equals(op.getOp())) { - if (op.getPath().contains(UPLOAD_STEP_METADATA_PATH)) { + if (UPDATE_METADATA_PATTERN.matcher(op.getPath()).matches()) { instance = UPLOAD_STEP_METADATA_OPERATION_ENTRY; - } else if (op.getPath().contains(UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY)) { + } else if (ACCESS_CONDITION_PATTERN.matcher(op.getPath()).matches()) { instance = stepConf.getType() + "." + UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY; } else { instance = UPLOAD_STEP_REMOVE_OPERATION_ENTRY; } } else if ("move".equals(op.getOp())) { - if (op.getPath().contains(UPLOAD_STEP_METADATA_PATH)) { + if (UPDATE_METADATA_PATTERN.matcher(op.getPath()).matches()) { instance = UPLOAD_STEP_METADATA_OPERATION_ENTRY; } else { instance = UPLOAD_STEP_MOVE_OPERATION_ENTRY; } } else { - if (op.getPath().contains(UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY)) { + if (ACCESS_CONDITION_PATTERN.matcher(op.getPath()).matches()) { instance = stepConf.getType() + "." + UPLOAD_STEP_ACCESSCONDITIONS_OPERATION_ENTRY; - } else if (op.getPath().contains(UPLOAD_STEP_METADATA_PATH)) { + } else if (UPDATE_METADATA_PATTERN.matcher(op.getPath()).matches()) { instance = UPLOAD_STEP_METADATA_OPERATION_ENTRY; } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java index fe8e67bbf564..de98f1b0cea6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java @@ -9,6 +9,7 @@ import static com.jayway.jsonpath.JsonPath.read; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -2218,4 +2219,49 @@ public void testWorkflowWithHiddenSections() throws Exception { } + @Test + public void deleteBitstreamTest() + throws Exception { + context.turnOffAuthorisationSystem(); + EPerson submitter = EPersonBuilder.createEPerson(context) + .withEmail("submitter@example.com") + .withPassword(password) + .build(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection collection1 = CollectionBuilder.createCollection(context, parentCommunity, + "123456789/collection-test-patch") + .withName("Collection 1") + .build(); + Bitstream bitstream = null; + WorkspaceItem witem = null; + String bitstreamContent = "0123456789"; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, Charset.defaultCharset())) { + context.setCurrentUser(submitter); + witem = WorkspaceItemBuilder.createWorkspaceItem(context, collection1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2019-10-01") + .grantLicense() + .build(); + bitstream = BitstreamBuilder.createBitstream(context, witem.getItem(), is) + .withName("Test bitstream") + .withDescription("This is a bitstream to test range requests") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + String tokenSubmitter = getAuthToken(submitter.getEmail(), password); + List deleteFile = new ArrayList<>(); + deleteFile.add(new RemoveOperation("/sections/bitstream-metadata-publication/files/0/")); + getClient(tokenSubmitter).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(getPatchContent(deleteFile)) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()); + // verify that the patch removed bitstream + getClient(tokenSubmitter).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.bitstream-metadata-publication.files",hasSize(0))); + } + } From 0ef3b27189dab67f3176831168f8ee4fc26d0c96 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Wed, 26 Feb 2025 11:18:36 +0100 Subject: [PATCH 108/701] [DURACOM-328] fix test --- .../org/dspace/app/rest/WorkflowItemRestRepositoryIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java index de98f1b0cea6..8a19678f5102 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java @@ -2253,7 +2253,7 @@ public void deleteBitstreamTest() context.restoreAuthSystemState(); String tokenSubmitter = getAuthToken(submitter.getEmail(), password); List deleteFile = new ArrayList<>(); - deleteFile.add(new RemoveOperation("/sections/bitstream-metadata-publication/files/0/")); + deleteFile.add(new RemoveOperation("/sections/upload-no-required-metadata/files/0/")); getClient(tokenSubmitter).perform(patch("/api/submission/workspaceitems/" + witem.getID()) .content(getPatchContent(deleteFile)) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) @@ -2261,7 +2261,7 @@ public void deleteBitstreamTest() // verify that the patch removed bitstream getClient(tokenSubmitter).perform(get("/api/submission/workspaceitems/" + witem.getID())) .andExpect(status().isOk()) - .andExpect(jsonPath("$.sections.bitstream-metadata-publication.files",hasSize(0))); + .andExpect(jsonPath("$.sections.upload-no-required-metadata.files",hasSize(0))); } } From ac20eefe4b6554439ac54c5ffe95348ec8466a7a Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Wed, 7 May 2025 10:43:19 +0200 Subject: [PATCH 109/701] [DURACOM-328] fix tests --- .../org/dspace/app/rest/SubmissionDefinitionsControllerIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index 1a1a4576dc89..a4ef63afa6ef 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -35,7 +35,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra // The total number of expected submission definitions is referred to in multiple tests and assertions as // is the last page (totalDefinitions - 1) // This integer should be maintained along with any changes to item-submissions.xml - private static final int totalDefinitions = 9; + private static final int totalDefinitions = 10; @Test public void findAll() throws Exception { From 1ade96098802cd30880b75dee37cbe92cca6542c Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Wed, 7 May 2025 12:28:43 +0200 Subject: [PATCH 110/701] [DURACOM-109] Fixed conflicts --- .../org/dspace/app/sherpa/SHERPAService.java | 118 +++++++++--------- .../dspace/eperson/CaptchaServiceImpl.java | 19 ++- .../external/OpenAIRERestConnector.java | 118 +++++++++--------- .../client/GoogleAnalyticsClientImpl.java | 4 +- .../identifier/doi/DataCiteConnector.java | 4 +- .../dspace/identifier/ezid/EZIDRequest.java | 6 +- .../model/factory/OrcidFactoryUtils.java | 4 +- 7 files changed, 138 insertions(+), 135 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java index 87042c502d83..cb1125f61c46 100644 --- a/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java +++ b/dspace-api/src/main/java/org/dspace/app/sherpa/SHERPAService.java @@ -117,46 +117,47 @@ public SHERPAPublisherResponse performPublisherRequest(String type, String field timeout, sleepBetweenTimeouts)); - try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5)) { Thread.sleep(sleepBetweenTimeouts); // Construct a default HTTP method (first result) method = constructHttpGet(type, field, predicate, value, start, limit); // Execute the method - CloseableHttpResponse response = client.execute(method); - int statusCode = response.getStatusLine().getStatusCode(); + try (CloseableHttpResponse response = client.execute(method)) { + int statusCode = response.getStatusLine().getStatusCode(); - log.debug(response.getStatusLine().getStatusCode() + ": " - + response.getStatusLine().getReasonPhrase()); + log.debug(response.getStatusLine().getStatusCode() + ": " + + response.getStatusLine().getReasonPhrase()); - if (statusCode != HttpStatus.SC_OK) { - sherpaResponse = new SHERPAPublisherResponse("SHERPA/RoMEO return not OK status: " - + statusCode); - String errorBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - log.error("Error from SHERPA HTTP request: " + errorBody); - } + if (statusCode != HttpStatus.SC_OK) { + sherpaResponse = new SHERPAPublisherResponse("SHERPA/RoMEO return not OK status: " + + statusCode); + String errorBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + log.error("Error from SHERPA HTTP request: " + errorBody); + } - HttpEntity responseBody = response.getEntity(); - - // If the response body is valid, pass to SHERPAResponse for parsing as JSON - if (null != responseBody) { - log.debug("Non-null SHERPA resonse received for query of " + value); - InputStream content = null; - try { - content = responseBody.getContent(); - sherpaResponse = - new SHERPAPublisherResponse(content, SHERPAPublisherResponse.SHERPAFormat.JSON); - } catch (IOException e) { - log.error("Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(), e); - } finally { - if (content != null) { - content.close(); + HttpEntity responseBody = response.getEntity(); + + // If the response body is valid, pass to SHERPAResponse for parsing as JSON + if (null != responseBody) { + log.debug("Non-null SHERPA response received for query of " + value); + InputStream content = null; + try { + content = responseBody.getContent(); + sherpaResponse = + new SHERPAPublisherResponse(content, SHERPAPublisherResponse.SHERPAFormat.JSON); + } catch (IOException e) { + log.error("Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(), e); + } finally { + if (content != null) { + content.close(); + } } + } else { + log.debug("Empty SHERPA response body for query on " + value); + sherpaResponse = new SHERPAPublisherResponse("SHERPA/RoMEO returned no response"); } - } else { - log.debug("Empty SHERPA response body for query on " + value); - sherpaResponse = new SHERPAPublisherResponse("SHERPA/RoMEO returned no response"); } } catch (URISyntaxException e) { String errorMessage = "Error building SHERPA v2 API URI: " + e.getMessage(); @@ -220,45 +221,46 @@ public SHERPAResponse performRequest(String type, String field, String predicate timeout, sleepBetweenTimeouts)); - try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().build()) { + try (CloseableHttpClient client = DSpaceHttpClientFactory.getInstance().buildWithoutAutomaticRetries(5)) { Thread.sleep(sleepBetweenTimeouts); // Construct a default HTTP method (first result) method = constructHttpGet(type, field, predicate, value, start, limit); // Execute the method - CloseableHttpResponse response = client.execute(method); - int statusCode = response.getStatusLine().getStatusCode(); + try (CloseableHttpResponse response = client.execute(method)) { + int statusCode = response.getStatusLine().getStatusCode(); - log.debug(response.getStatusLine().getStatusCode() + ": " - + response.getStatusLine().getReasonPhrase()); + log.debug(response.getStatusLine().getStatusCode() + ": " + + response.getStatusLine().getReasonPhrase()); - if (statusCode != HttpStatus.SC_OK) { - sherpaResponse = new SHERPAResponse("SHERPA/RoMEO return not OK status: " - + statusCode); - String errorBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); - log.error("Error from SHERPA HTTP request: " + errorBody); - } + if (statusCode != HttpStatus.SC_OK) { + sherpaResponse = new SHERPAResponse("SHERPA/RoMEO return not OK status: " + + statusCode); + String errorBody = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + log.error("Error from SHERPA HTTP request: " + errorBody); + } - HttpEntity responseBody = response.getEntity(); - - // If the response body is valid, pass to SHERPAResponse for parsing as JSON - if (null != responseBody) { - log.debug("Non-null SHERPA resonse received for query of " + value); - InputStream content = null; - try { - content = responseBody.getContent(); - sherpaResponse = new SHERPAResponse(content, SHERPAResponse.SHERPAFormat.JSON); - } catch (IOException e) { - log.error("Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(), e); - } finally { - if (content != null) { - content.close(); + HttpEntity responseBody = response.getEntity(); + + // If the response body is valid, pass to SHERPAResponse for parsing as JSON + if (null != responseBody) { + log.debug("Non-null SHERPA response received for query of " + value); + InputStream content = null; + try { + content = responseBody.getContent(); + sherpaResponse = new SHERPAResponse(content, SHERPAResponse.SHERPAFormat.JSON); + } catch (IOException e) { + log.error("Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(), e); + } finally { + if (content != null) { + content.close(); + } } + } else { + log.debug("Empty SHERPA response body for query on " + value); + sherpaResponse = new SHERPAResponse("SHERPA/RoMEO returned no response"); } - } else { - log.debug("Empty SHERPA response body for query on " + value); - sherpaResponse = new SHERPAResponse("SHERPA/RoMEO returned no response"); } } catch (URISyntaxException e) { String errorMessage = "Error building SHERPA v2 API URI: " + e.getMessage(); @@ -268,7 +270,7 @@ public SHERPAResponse performRequest(String type, String field, String predicate String errorMessage = "Encountered exception while contacting SHERPA/RoMEO: " + e.getMessage(); log.error(errorMessage, e); sherpaResponse = new SHERPAResponse(errorMessage); - } catch (InterruptedException e) { + } catch (InterruptedException e) { String errorMessage = "Encountered exception while sleeping thread: " + e.getMessage(); log.error(errorMessage, e); sherpaResponse = new SHERPAResponse(errorMessage); diff --git a/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java index e10de57e47a6..3e967d59b6f3 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/CaptchaServiceImpl.java @@ -17,11 +17,11 @@ import javax.annotation.PostConstruct; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; -import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -82,18 +82,17 @@ public void processResponse(String response, String action) throws InvalidReCapt throw new RuntimeException(e.getMessage(), e); } - HttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); - HttpResponse httpResponse; - GoogleCaptchaResponse googleResponse; - final ObjectMapper objectMapper = new ObjectMapper(); - try { - httpResponse = httpClient.execute(httpPost); - googleResponse = objectMapper.readValue(httpResponse.getEntity().getContent(), GoogleCaptchaResponse.class); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + final ObjectMapper objectMapper = new ObjectMapper(); + try (CloseableHttpResponse httpResponse = httpClient.execute(httpPost)) { + GoogleCaptchaResponse googleResponse = objectMapper.readValue(httpResponse.getEntity().getContent(), + GoogleCaptchaResponse.class); + validateGoogleResponse(googleResponse, action); + } } catch (IOException e) { log.error(e.getMessage(), e); throw new RuntimeException("Error during verify google recaptcha site", e); } - validateGoogleResponse(googleResponse, action); } private boolean responseSanityCheck(String response) { diff --git a/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java b/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java index 06a4b8361436..8b5fb1e523c5 100644 --- a/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java @@ -28,10 +28,10 @@ import org.apache.http.NameValuePair; import org.apache.http.NoHttpResponseException; import org.apache.http.StatusLine; -import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -121,33 +121,34 @@ public OpenAIRERestToken grabNewAccessToken() throws IOException { params.add(new BasicNameValuePair("grant_type", "client_credentials")); httpPost.setEntity(new UrlEncodedFormEntity(params, "UTF-8")); - HttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); - HttpResponse getResponse = httpClient.execute(httpPost); - - JSONObject responseObject = null; - try (InputStream is = getResponse.getEntity().getContent(); - BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { - String inputStr; - // verify if we have basic json - while ((inputStr = streamReader.readLine()) != null && responseObject == null) { - if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token") - && inputStr.contains("expires_in")) { - try { - responseObject = new JSONObject(inputStr); - } catch (Exception e) { - // Not as valid as I'd hoped, move along - responseObject = null; + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + HttpResponse getResponse = httpClient.execute(httpPost); + + JSONObject responseObject = null; + try (InputStream is = getResponse.getEntity().getContent(); + BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { + String inputStr; + // verify if we have basic json + while ((inputStr = streamReader.readLine()) != null && responseObject == null) { + if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token") + && inputStr.contains("expires_in")) { + try { + responseObject = new JSONObject(inputStr); + } catch (Exception e) { + // Not as valid as I'd hoped, move along + responseObject = null; + } } } } - } - if (responseObject == null || !responseObject.has("access_token") || !responseObject.has("expires_in")) { - throw new IOException("Unable to grab the access token using provided service url, client id and secret"); - } - - return new OpenAIRERestToken(responseObject.get("access_token").toString(), - Long.valueOf(responseObject.get("expires_in").toString())); + if (responseObject == null || !responseObject.has("access_token") || !responseObject.has("expires_in")) { + throw new IOException("Unable to grab the access token using provided service url, " + + "client id and secret"); + } + return new OpenAIRERestToken(responseObject.get("access_token").toString(), + Long.valueOf(responseObject.get("expires_in").toString())); + } } /** @@ -172,42 +173,43 @@ public InputStream get(String file, String accessToken) { httpGet.addHeader("Authorization", "Bearer " + accessToken); } - HttpClient httpClient = DSpaceHttpClientFactory.getInstance().build(); - getResponse = httpClient.execute(httpGet); - - StatusLine status = getResponse.getStatusLine(); - - // registering errors - switch (status.getStatusCode()) { - case HttpStatus.SC_NOT_FOUND: - // 404 - Not found - case HttpStatus.SC_FORBIDDEN: - // 403 - Invalid Access Token - case 429: - // 429 - Rate limit abuse for unauthenticated user - Header[] limitUsed = getResponse.getHeaders("x-ratelimit-used"); - Header[] limitMax = getResponse.getHeaders("x-ratelimit-limit"); - - if (limitUsed.length > 0) { - String limitMsg = limitUsed[0].getValue(); - if (limitMax.length > 0) { - limitMsg = limitMsg.concat(" of " + limitMax[0].getValue()); + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + getResponse = httpClient.execute(httpGet); + + StatusLine status = getResponse.getStatusLine(); + + // registering errors + switch (status.getStatusCode()) { + case HttpStatus.SC_NOT_FOUND: + // 404 - Not found + case HttpStatus.SC_FORBIDDEN: + // 403 - Invalid Access Token + case 429: + // 429 - Rate limit abuse for unauthenticated user + Header[] limitUsed = getResponse.getHeaders("x-ratelimit-used"); + Header[] limitMax = getResponse.getHeaders("x-ratelimit-limit"); + + if (limitUsed.length > 0) { + String limitMsg = limitUsed[0].getValue(); + if (limitMax.length > 0) { + limitMsg = limitMsg.concat(" of " + limitMax[0].getValue()); + } + getGotError(new NoHttpResponseException(status.getReasonPhrase() + " with usage limit " + + limitMsg), + url + '/' + file); + } else { + // 429 - Rate limit abuse + getGotError(new NoHttpResponseException(status.getReasonPhrase()), url + '/' + file); } - getGotError( - new NoHttpResponseException(status.getReasonPhrase() + " with usage limit " + limitMsg), - url + '/' + file); - } else { - // 429 - Rate limit abuse - getGotError(new NoHttpResponseException(status.getReasonPhrase()), url + '/' + file); - } - break; - default: - // 200 or other - break; - } + break; + default: + // 200 or other + break; + } - // do not close this httpClient - result = getResponse.getEntity().getContent(); + // do not close this httpClient + result = getResponse.getEntity().getContent(); + } } catch (MalformedURLException e1) { getGotError(e1, url + '/' + file); } catch (Exception e) { diff --git a/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java b/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java index b5ee1806cd56..6fa748cfd450 100644 --- a/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java @@ -18,7 +18,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.google.GoogleAnalyticsEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +42,7 @@ public class GoogleAnalyticsClientImpl implements GoogleAnalyticsClient { public GoogleAnalyticsClientImpl(String keyPrefix, GoogleAnalyticsClientRequestBuilder requestBuilder) { this.keyPrefix = keyPrefix; this.requestBuilder = requestBuilder; - this.httpclient = HttpClients.createDefault(); + this.httpclient = DSpaceHttpClientFactory.getInstance().build(); } @Override diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index 5bb37add2d9a..86c7b8322e81 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java @@ -36,8 +36,8 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.crosswalk.CrosswalkException; @@ -722,7 +722,7 @@ protected DataCiteResponse sendHttpRequest(HttpUriRequest req, String doi) httpContext.setCredentialsProvider(credentialsProvider); HttpEntity entity = null; - try ( CloseableHttpClient httpclient = HttpClientBuilder.create().build(); ) { + try (CloseableHttpClient httpclient = DSpaceHttpClientFactory.getInstance().build()) { HttpResponse response = httpclient.execute(req, httpContext); StatusLine status = response.getStatusLine(); diff --git a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java index 525ad46b2554..8da40470d6a1 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java +++ b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java @@ -26,7 +26,7 @@ import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.identifier.DOI; import org.dspace.identifier.IdentifierException; import org.slf4j.Logger; @@ -87,7 +87,7 @@ public class EZIDRequest { this.authority = authority; } - client = HttpClientBuilder.create().build(); + client = DSpaceHttpClientFactory.getInstance().build(); httpContext = HttpClientContext.create(); if (null != username) { URI uri = new URI(scheme, host, path, null); @@ -124,7 +124,7 @@ public class EZIDRequest { this.authority = authority; } - client = HttpClientBuilder.create().build(); + client = DSpaceHttpClientFactory.getInstance().build(); httpContext = HttpClientContext.create(); if (null != username) { CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java index 38aa611ff3a0..ce68ab47c26e 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java @@ -20,7 +20,7 @@ import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.json.JSONObject; /** @@ -97,7 +97,7 @@ public static Optional retrieveAccessToken(String clientId, String clien httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); HttpResponse response; - try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { response = httpClient.execute(httpPost); } JSONObject responseObject = null; From 2c6f02f74c64603c830eaf510d43883147959e30 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Fri, 9 May 2025 15:59:50 +0200 Subject: [PATCH 111/701] [DURACOM-109] fix missing dependency --- dspace-api/pom.xml | 10 ++++++++++ .../org/dspace/google/GoogleRecorderEventListener.java | 4 ++-- pom.xml | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6898bc13fbbe..44bbbeee1926 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -521,6 +521,10 @@ org.apache.pdfbox pdfbox + + org.apache.pdfbox + fontbox + com.ibm.icu icu4j @@ -937,6 +941,12 @@ 2.13.16 test + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + diff --git a/dspace-api/src/main/java/org/dspace/google/GoogleRecorderEventListener.java b/dspace-api/src/main/java/org/dspace/google/GoogleRecorderEventListener.java index 4159661b1ced..90248321e5e9 100644 --- a/dspace-api/src/main/java/org/dspace/google/GoogleRecorderEventListener.java +++ b/dspace-api/src/main/java/org/dspace/google/GoogleRecorderEventListener.java @@ -21,9 +21,9 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicNameValuePair; import org.apache.logging.log4j.Logger; +import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Constants; import org.dspace.service.ClientInfoService; @@ -56,7 +56,7 @@ public class GoogleRecorderEventListener extends AbstractUsageEventListener { public GoogleRecorderEventListener() { // httpclient is threadsafe so we only need one. - httpclient = HttpClients.createDefault(); + httpclient = DSpaceHttpClientFactory.getInstance().build(); } @Autowired diff --git a/pom.xml b/pom.xml index 107318fccb15..132b3bb4e1f9 100644 --- a/pom.xml +++ b/pom.xml @@ -1754,7 +1754,12 @@ google-http-client-gson 1.47.0 - + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + com.google.code.findbugs From 53e41479107aefffbf571e6dceb52834f8f4c674 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Fri, 9 May 2025 16:12:50 +0200 Subject: [PATCH 112/701] [DURACOM-109] fix dependency error --- dspace-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 44bbbeee1926..5f8943bcdb98 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -871,6 +871,12 @@ + + + com.squareup.okhttp3 + mockwebserver + test + @@ -941,12 +947,6 @@ 2.13.16 test - - com.squareup.okhttp3 - mockwebserver - 4.12.0 - test - From 086a26d3b489fad3026f0f3cbd64681558965163 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 9 May 2025 12:32:19 -0500 Subject: [PATCH 113/701] Potential fix for code scanning alert no. 3549: Arbitrary file access during archive extraction ("Zip Slip") Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> (cherry picked from commit 5fbdfc218f0c4da45fc430f5ee50c3813e7bb851) --- .../org/dspace/sword2/SimpleZipContentIngester.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SimpleZipContentIngester.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SimpleZipContentIngester.java index bd8301617ba9..7899bf861b32 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SimpleZipContentIngester.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SimpleZipContentIngester.java @@ -138,12 +138,18 @@ private List unzipToBundle(Context context, File depositFile, Enumeration zenum = zip.entries(); while (zenum.hasMoreElements()) { ZipEntry entry = (ZipEntry) zenum.nextElement(); + String entryName = entry.getName(); + java.nio.file.Path entryPath = java.nio.file.Paths.get(entryName).normalize(); + if (entryPath.isAbsolute() || entryPath.startsWith("..")) { + throw new SwordError(UriRegistry.ERROR_BAD_REQUEST, "Invalid zip entry: " + entryName); + } + InputStream stream = zip.getInputStream(entry); Bitstream bs = bitstreamService.create(context, target, stream); BitstreamFormat format = this - .getFormat(context, entry.getName()); + .getFormat(context, entryName); bs.setFormat(context, format); - bs.setName(context, entry.getName()); + bs.setName(context, entryName); bitstreamService.update(context, bs); derivedResources.add(bs); } From 42e979a021191a545df073099c358c69293bcfea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 22:48:26 +0000 Subject: [PATCH 114/701] Bump the test-tools group with 6 updates Bumps the test-tools group with 6 updates: | Package | From | To | | --- | --- | --- | | [io.netty:netty-buffer](https://github.com/netty/netty) | `4.2.0.Final` | `4.2.1.Final` | | [io.netty:netty-transport](https://github.com/netty/netty) | `4.2.0.Final` | `4.2.1.Final` | | [io.netty:netty-transport-native-unix-common](https://github.com/netty/netty) | `4.2.0.Final` | `4.2.1.Final` | | [io.netty:netty-common](https://github.com/netty/netty) | `4.2.0.Final` | `4.2.1.Final` | | [io.netty:netty-handler](https://github.com/netty/netty) | `4.2.0.Final` | `4.2.1.Final` | | [io.netty:netty-codec](https://github.com/netty/netty) | `4.2.0.Final` | `4.2.1.Final` | Updates `io.netty:netty-buffer` from 4.2.0.Final to 4.2.1.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.0.Final...netty-4.2.1.Final) Updates `io.netty:netty-transport` from 4.2.0.Final to 4.2.1.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.0.Final...netty-4.2.1.Final) Updates `io.netty:netty-transport-native-unix-common` from 4.2.0.Final to 4.2.1.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.0.Final...netty-4.2.1.Final) Updates `io.netty:netty-common` from 4.2.0.Final to 4.2.1.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.0.Final...netty-4.2.1.Final) Updates `io.netty:netty-handler` from 4.2.0.Final to 4.2.1.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.0.Final...netty-4.2.1.Final) Updates `io.netty:netty-codec` from 4.2.0.Final to 4.2.1.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.0.Final...netty-4.2.1.Final) --- updated-dependencies: - dependency-name: io.netty:netty-buffer dependency-version: 4.2.1.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport dependency-version: 4.2.1.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport-native-unix-common dependency-version: 4.2.1.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-common dependency-version: 4.2.1.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-handler dependency-version: 4.2.1.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-codec dependency-version: 4.2.1.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 5f8943bcdb98..05cb803cdb85 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -888,32 +888,32 @@ io.netty netty-buffer - 4.2.0.Final + 4.2.1.Final io.netty netty-transport - 4.2.0.Final + 4.2.1.Final io.netty netty-transport-native-unix-common - 4.2.0.Final + 4.2.1.Final io.netty netty-common - 4.2.0.Final + 4.2.1.Final io.netty netty-handler - 4.2.0.Final + 4.2.1.Final io.netty netty-codec - 4.2.0.Final + 4.2.1.Final org.apache.velocity From 5765bff79ce9e8f35bd23ea9db761160b50882aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 22:49:31 +0000 Subject: [PATCH 115/701] Bump org.webjars.npm:json-editor__json-editor in the webjars group Bumps the webjars group with 1 update: [org.webjars.npm:json-editor__json-editor](https://github.com/json-editor/json-editor). Updates `org.webjars.npm:json-editor__json-editor` from 2.15.1 to 2.15.2 - [Changelog](https://github.com/json-editor/json-editor/blob/master/CHANGELOG.md) - [Commits](https://github.com/json-editor/json-editor/compare/2.15.1...2.15.2) --- updated-dependencies: - dependency-name: org.webjars.npm:json-editor__json-editor dependency-version: 2.15.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: webjars ... Signed-off-by: dependabot[bot] --- dspace-server-webapp/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 52105803889e..15ee58e6e1bc 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -364,7 +364,7 @@ org.webjars.npm json-editor__json-editor - 2.15.1 + 2.15.2 ${basedir}/.. - 3.4.0 + 3.4.1 5.87.0.RELEASE From a2a68383904566c186bf6374ad86ed295d8dd896 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 22:15:28 +0000 Subject: [PATCH 125/701] Bump org.xmlunit:xmlunit-core from 2.10.0 to 2.10.2 Bumps [org.xmlunit:xmlunit-core](https://github.com/xmlunit/xmlunit) from 2.10.0 to 2.10.2. - [Release notes](https://github.com/xmlunit/xmlunit/releases) - [Changelog](https://github.com/xmlunit/xmlunit/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/xmlunit/xmlunit/compare/v2.10.0...v2.10.2) --- updated-dependencies: - dependency-name: org.xmlunit:xmlunit-core dependency-version: 2.10.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 05cb803cdb85..58ac06c44f76 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -923,7 +923,7 @@ org.xmlunit xmlunit-core - 2.10.0 + 2.10.2 test From 3fef856ef06a85befb99003b24fad030db17e38a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 16:25:45 +0000 Subject: [PATCH 126/701] Bump commons-beanutils:commons-beanutils in the apache-commons group Bumps the apache-commons group with 1 update: commons-beanutils:commons-beanutils. Updates `commons-beanutils:commons-beanutils` from 1.10.1 to 1.11.0 --- updated-dependencies: - dependency-name: commons-beanutils:commons-beanutils dependency-version: 1.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 132b3bb4e1f9..a6c30c0e57fd 100644 --- a/pom.xml +++ b/pom.xml @@ -1470,7 +1470,7 @@ commons-beanutils commons-beanutils - 1.10.1 + 1.11.0 commons-cli From b7960bd42e726f4607b4c41cc3e839d471e30f7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 16:26:54 +0000 Subject: [PATCH 127/701] Bump io.grpc:grpc-context from 1.72.0 to 1.73.0 Bumps [io.grpc:grpc-context](https://github.com/grpc/grpc-java) from 1.72.0 to 1.73.0. - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.72.0...v1.73.0) --- updated-dependencies: - dependency-name: io.grpc:grpc-context dependency-version: 1.73.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 132b3bb4e1f9..86fec0602c8a 100644 --- a/pom.xml +++ b/pom.xml @@ -1725,7 +1725,7 @@ io.grpc grpc-context - 1.72.0 + 1.73.0 com.google.http-client From 4367eebef221d475733160b654d8afbcafaaf285 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 May 2025 16:28:25 +0000 Subject: [PATCH 128/701] Bump org.postgresql:postgresql from 42.7.5 to 42.7.6 Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.5 to 42.7.6. - [Release notes](https://github.com/pgjdbc/pgjdbc/releases) - [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md) - [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.5...REL42.7.6) --- updated-dependencies: - dependency-name: org.postgresql:postgresql dependency-version: 42.7.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 132b3bb4e1f9..59b126b10953 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ 5.7.14 5.6.15.Final 6.2.5.Final - 42.7.5 + 42.7.6 8.11.4 3.10.8 From 32bd615ba8dee1ae433799e75d2dea70607d6700 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 02:30:57 +0000 Subject: [PATCH 129/701] Bump com.opencsv:opencsv from 5.11 to 5.11.1 Bumps com.opencsv:opencsv from 5.11 to 5.11.1. --- updated-dependencies: - dependency-name: com.opencsv:opencsv dependency-version: 5.11.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 58ac06c44f76..f89aee3074a8 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -805,7 +805,7 @@ com.opencsv opencsv - 5.11 + 5.11.1 From 19e22c10f1a4ad2b2de4d90b97485ed97b826ff0 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 2 Jun 2025 09:16:27 -0500 Subject: [PATCH 130/701] Alphabetize importers by service name --- .../config/spring/api/external-services.xml | 196 ++++++++++-------- 1 file changed, 105 insertions(+), 91 deletions(-) diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 6d7d50c39f1b..4792a4fd8589 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -7,72 +7,62 @@ - - - - - - - - - + + + + + - Journal + Publication + none - - - - - - - - - + + + + + + - Journal + Publication + none - - - - - - - - - + + + + + + - OrgUnit + Publication + none - - - - - - - + + + + + + - Person + Publication + none - - - - - - - + + + + @@ -82,9 +72,10 @@ - - - + + + + @@ -93,7 +84,8 @@ - + + @@ -104,21 +96,28 @@ - - - - + + + + + + + - Publication - none + Person - - - + + + + + + + + @@ -140,9 +139,10 @@ - - - + + + + @@ -152,9 +152,10 @@ - - - + + + + @@ -164,45 +165,57 @@ - - - - + + + + + + + + + + - Publication - none + Journal - - - - - + + + + + + + + + - Publication - none + Journal - - - - - + + + + + + + + + - Publication - none + OrgUnit - - - + + + + @@ -212,10 +225,11 @@ - - - - + + + + + Publication From 25ad6039dcfe93e7fce3fb6ebe029650ce0e43e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:25:05 +0000 Subject: [PATCH 131/701] Bump org.apache.maven.plugins:maven-clean-plugin Bumps the build-tools group with 1 update: [org.apache.maven.plugins:maven-clean-plugin](https://github.com/apache/maven-clean-plugin). Updates `org.apache.maven.plugins:maven-clean-plugin` from 3.4.1 to 3.5.0 - [Release notes](https://github.com/apache/maven-clean-plugin/releases) - [Commits](https://github.com/apache/maven-clean-plugin/compare/maven-clean-plugin-3.4.1...maven-clean-plugin-3.5.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-clean-plugin dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fc0df5c2de29..af2c99bb775b 100644 --- a/pom.xml +++ b/pom.xml @@ -321,7 +321,7 @@ maven-clean-plugin - 3.4.1 + 3.5.0 From 21c56aeda7fa07c35b97510bc963f0cd05f2395e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:25:16 +0000 Subject: [PATCH 132/701] Bump bouncycastle.version from 1.80 to 1.81 Bumps `bouncycastle.version` from 1.80 to 1.81. Updates `org.bouncycastle:bcpkix-jdk18on` from 1.80 to 1.81 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `org.bouncycastle:bcprov-jdk18on` from 1.80 to 1.81 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `org.bouncycastle:bcutil-jdk18on` from 1.80 to 1.81 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) --- updated-dependencies: - dependency-name: org.bouncycastle:bcpkix-jdk18on dependency-version: '1.81' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.bouncycastle:bcprov-jdk18on dependency-version: '1.81' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.bouncycastle:bcutil-jdk18on dependency-version: '1.81' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fc0df5c2de29..ad009b631e1b 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 1.7.36 2.9.4 - 1.80 + 1.81 From a6cc912e6230f2bfa8d4e06a4f8794d526d5e191 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:25:57 +0000 Subject: [PATCH 133/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.783 to 1.12.785 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.783 to 1.12.785. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.783...1.12.785) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-version: 1.12.785 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index f89aee3074a8..6d5ff4306bc5 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -756,7 +756,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.783 + 1.12.785 From 87ce9fd136c0c5293872af6eb3009d9d15b7ec33 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Wed, 28 May 2025 17:27:07 +0200 Subject: [PATCH 134/701] Add HTTP timeouts to improve robustness (cherry picked from commit cabf5a7a44b29cd022a4c78690818bce7e5e704c) --- .../liveimportclient/service/LiveImportClientImpl.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java index 84475b62c07b..987f2fde34dc 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java @@ -50,7 +50,11 @@ public class LiveImportClientImpl implements LiveImportClient { @Override public String executeHttpGetRequest(int timeout, String URL, Map> params) { HttpGet method = null; - RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(timeout).build(); + RequestConfig config = RequestConfig.custom() + .setConnectionRequestTimeout(timeout) + .setConnectTimeout(timeout) + .setSocketTimeout(timeout) + .build(); try (CloseableHttpClient httpClient = Optional.ofNullable(this.httpClient) .orElse(DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config))) { String uri = buildUrl(URL, params.get(URI_PARAMETERS)); From 0bc41bfaaf5285c5ba65f0a201f5007777cfe706 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 03:07:54 +0000 Subject: [PATCH 135/701] Bump the test-tools group with 6 updates Bumps the test-tools group with 6 updates: | Package | From | To | | --- | --- | --- | | [io.netty:netty-buffer](https://github.com/netty/netty) | `4.2.1.Final` | `4.2.2.Final` | | [io.netty:netty-transport](https://github.com/netty/netty) | `4.2.1.Final` | `4.2.2.Final` | | [io.netty:netty-transport-native-unix-common](https://github.com/netty/netty) | `4.2.1.Final` | `4.2.2.Final` | | [io.netty:netty-common](https://github.com/netty/netty) | `4.2.1.Final` | `4.2.2.Final` | | [io.netty:netty-handler](https://github.com/netty/netty) | `4.2.1.Final` | `4.2.2.Final` | | [io.netty:netty-codec](https://github.com/netty/netty) | `4.2.1.Final` | `4.2.2.Final` | Updates `io.netty:netty-buffer` from 4.2.1.Final to 4.2.2.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.1.Final...netty-4.2.2.Final) Updates `io.netty:netty-transport` from 4.2.1.Final to 4.2.2.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.1.Final...netty-4.2.2.Final) Updates `io.netty:netty-transport-native-unix-common` from 4.2.1.Final to 4.2.2.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.1.Final...netty-4.2.2.Final) Updates `io.netty:netty-common` from 4.2.1.Final to 4.2.2.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.1.Final...netty-4.2.2.Final) Updates `io.netty:netty-handler` from 4.2.1.Final to 4.2.2.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.1.Final...netty-4.2.2.Final) Updates `io.netty:netty-codec` from 4.2.1.Final to 4.2.2.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.1.Final...netty-4.2.2.Final) --- updated-dependencies: - dependency-name: io.netty:netty-buffer dependency-version: 4.2.2.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport dependency-version: 4.2.2.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport-native-unix-common dependency-version: 4.2.2.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-common dependency-version: 4.2.2.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-handler dependency-version: 4.2.2.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-codec dependency-version: 4.2.2.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6d5ff4306bc5..1f591e2ce2a2 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -888,32 +888,32 @@ io.netty netty-buffer - 4.2.1.Final + 4.2.2.Final io.netty netty-transport - 4.2.1.Final + 4.2.2.Final io.netty netty-transport-native-unix-common - 4.2.1.Final + 4.2.2.Final io.netty netty-common - 4.2.1.Final + 4.2.2.Final io.netty netty-handler - 4.2.1.Final + 4.2.2.Final io.netty netty-codec - 4.2.1.Final + 4.2.2.Final org.apache.velocity From a458fbd6ce0aa9a227153b969445534b9e23035e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 03:08:40 +0000 Subject: [PATCH 136/701] Bump org.codehaus.mojo:build-helper-maven-plugin Bumps the build-tools group with 1 update: [org.codehaus.mojo:build-helper-maven-plugin](https://github.com/mojohaus/build-helper-maven-plugin). Updates `org.codehaus.mojo:build-helper-maven-plugin` from 3.6.0 to 3.6.1 - [Release notes](https://github.com/mojohaus/build-helper-maven-plugin/releases) - [Commits](https://github.com/mojohaus/build-helper-maven-plugin/compare/3.6.0...3.6.1) --- updated-dependencies: - dependency-name: org.codehaus.mojo:build-helper-maven-plugin dependency-version: 3.6.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6d5ff4306bc5..95fd4c103daa 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -102,7 +102,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.6.0 + 3.6.1 validate From 358c00c1992053d2617bc1234f35d7d97caaa3a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 03:09:56 +0000 Subject: [PATCH 137/701] Bump net.handle:handle from 9.3.1 to 9.3.2 Bumps net.handle:handle from 9.3.1 to 9.3.2. --- updated-dependencies: - dependency-name: net.handle:handle dependency-version: 9.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index af2c99bb775b..8f9e210029e7 100644 --- a/pom.xml +++ b/pom.xml @@ -1382,7 +1382,7 @@ net.handle handle - 9.3.1 + 9.3.2 From a5ae3705e7ea3de503b6e4dd6e35fac3bacb4d96 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 9 Jun 2025 10:15:00 -0500 Subject: [PATCH 138/701] Remove commons-fileupload as it is no longer used. --- dspace-api/pom.xml | 4 ---- dspace-sword/pom.xml | 5 ----- pom.xml | 5 ----- 3 files changed, 14 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 756f65ff696c..7b3ab4a315e5 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -475,10 +475,6 @@ org.apache.commons commons-dbcp2 - - commons-fileupload - commons-fileupload - commons-io diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index d0ff123d741c..fc436f668921 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -54,11 +54,6 @@ - - - commons-fileupload - commons-fileupload - org.apache.httpcomponents httpclient diff --git a/pom.xml b/pom.xml index af2c99bb775b..cd7606e06c0a 100644 --- a/pom.xml +++ b/pom.xml @@ -1497,11 +1497,6 @@ commons-dbcp2 2.13.0 - - commons-fileupload - commons-fileupload - 1.5 - commons-io commons-io From 1f174f465715744d0fa743f464b38dd37db32c07 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Wed, 9 Apr 2025 17:06:43 +0200 Subject: [PATCH 139/701] 129944: Introduce custom abstract xpath contributor for pubmed to respect their labelled structure - modify IT for it (cherry picked from commit 28bc4970b7a5471f164d400a1bbd3e560b8e3e30) --- .../PubmedAbstractMetadatumContributor.java | 67 +++++++++++++++++++ .../PubmedImportMetadataSourceServiceIT.java | 64 ++++++++---------- .../app/rest/pubmedimport-search-test.xml | 4 +- .../config/spring/api/pubmed-integration.xml | 2 +- 4 files changed, 100 insertions(+), 37 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedAbstractMetadatumContributor.java diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedAbstractMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedAbstractMetadatumContributor.java new file mode 100644 index 000000000000..727d6b92ae35 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedAbstractMetadatumContributor.java @@ -0,0 +1,67 @@ +/** + * 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.importer.external.pubmed.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.metadatamapping.contributor.SimpleXpathMetadatumContributor; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; + +/** + * This class is responsible for extracting the abstract from a PubMed XML document. + * It uses XPath to find the relevant elements and constructs a formatted string for the abstract, respecting + * PubMed's labelled abstract format, and including the labels in the output. + */ +public class PubmedAbstractMetadatumContributor extends SimpleXpathMetadatumContributor { + + @Override + public Collection contributeMetadata(Element t) { + List values = new LinkedList<>(); + + List namespaces = new ArrayList<>(); + for (String ns : prefixToNamespaceMapping.keySet()) { + namespaces.add(Namespace.getNamespace(prefixToNamespaceMapping.get(ns), ns)); + } + + XPathExpression xpath = XPathFactory.instance().compile(query, Filters.element(), null, namespaces); + List nodes = xpath.evaluate(t); + StringBuilder sb = new StringBuilder(); + + for (Element el : nodes) { + String label = el.getAttributeValue("Label"); + String text = el.getTextNormalize(); + + if (text == null || text.isEmpty()) { + continue; + } + + if (sb.length() > 0) { + sb.append("\n\n"); + } + + if (label != null && !label.equalsIgnoreCase("UNLABELLED")) { + sb.append(label).append(": "); + } + sb.append(text); + } + + String fullAbstract = sb.toString().trim(); + if (!fullAbstract.isEmpty()) { + values.add(metadataFieldMapping.toDCValue(field, fullAbstract)); + } + return values; + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedImportMetadataSourceServiceIT.java index 3b39d251216c..5cac853c07b5 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedImportMetadataSourceServiceIT.java @@ -101,35 +101,34 @@ private ArrayList getRecords() { //define first record MetadatumDTO title = createMetadatumDTO("dc","title", null, "Teaching strategies of clinical reasoning in advanced nursing clinical practice: A scoping review."); - MetadatumDTO description1 = createMetadatumDTO("dc", "description", "abstract", "To report and synthesize" - + " the main strategies for teaching clinical reasoning described in the literature in the context of" - + " advanced clinical practice and promote new areas of research to improve the pedagogical approach" - + " to clinical reasoning in Advanced Practice Nursing."); - MetadatumDTO description2 = createMetadatumDTO("dc", "description", "abstract", "Clinical reasoning and" - + " clinical thinking are essential elements in the advanced nursing clinical practice decision-making" - + " process. The quality improvement of care is related to the development of those skills." - + " Therefore, it is crucial to optimize teaching strategies that can enhance the role of clinical" - + " reasoning in advanced clinical practice."); - MetadatumDTO description3 = createMetadatumDTO("dc", "description", "abstract", "A scoping review was" - + " conducted using the framework developed by Arksey and O'Malley as a research strategy." - + " Consistent with the nature of scoping reviews, a study protocol has been established."); - MetadatumDTO description4 = createMetadatumDTO("dc", "description", "abstract", "The studies included and" - + " analyzed in this scoping review cover from January 2016 to June 2022. Primary studies and secondary" - + " revision studies, published in biomedical databases, were selected, including qualitative ones." - + " Electronic databases used were: CINAHL, PubMed, Cochrane Library, Scopus, and OVID." - + " Three authors independently evaluated the articles for titles, abstracts, and full text."); - MetadatumDTO description5 = createMetadatumDTO("dc", "description", "abstract", "1433 articles were examined," - + " applying the eligibility and exclusion criteria 73 studies were assessed for eligibility," - + " and 27 were included in the scoping review. The results that emerged from the review were" - + " interpreted and grouped into three macro strategies (simulations-based education, art and visual" - + " thinking, and other learning approaches) and nineteen educational interventions."); - MetadatumDTO description6 = createMetadatumDTO("dc", "description", "abstract", "Among the different" - + " strategies, the simulations are the most used. Despite this, our scoping review reveals that is" - + " necessary to use different teaching strategies to stimulate critical thinking, improve diagnostic" - + " reasoning, refine clinical judgment, and strengthen decision-making. However, it is not possible to" - + " demonstrate which methodology is more effective in obtaining the learning outcomes necessary to" - + " acquire an adequate level of judgment and critical thinking. Therefore, it will be" - + " necessary to relate teaching methodologies with the skills developed."); + MetadatumDTO description1 = createMetadatumDTO("dc", "description", "abstract", + "AIM/OBJECTIVE: To report and synthesize the main strategies for teaching clinical reasoning " + + "described in the literature in the context of advanced clinical practice and promote new " + + "areas of research to improve the pedagogical approach to clinical reasoning in Advanced " + + "Practice Nursing.\n\nBACKGROUND: Clinical reasoning and clinical thinking are essential " + + "elements in the advanced nursing clinical practice decision-making process. The quality " + + "improvement of care is related to the development of those skills. Therefore, it is crucial " + + "to optimize teaching strategies that can enhance the role of clinical reasoning in advanced " + + "clinical practice.\n\nDESIGN: A scoping review was conducted using the framework developed " + + "by Arksey and O'Malley as a research strategy. Consistent with the nature of scoping reviews" + + ", a study protocol has been established.\n\nMETHODS: The studies included and analyzed in " + + "this scoping review cover from January 2016 to June 2022. Primary studies and secondary " + + "revision studies, published in biomedical databases, were selected, including qualitative " + + "ones. Electronic databases used were: CINAHL, PubMed, Cochrane Library, Scopus, and OVID. " + + "Three authors independently evaluated the articles for titles, abstracts, and full text.\n\n" + + "RESULTS: 1433 articles were examined, applying the eligibility and exclusion criteria 73 " + + "studies were assessed for eligibility, and 27 were included in the scoping review. The " + + "results that emerged from the review were interpreted and grouped into three macro " + + "strategies (simulations-based education, art and visual thinking, and other learning " + + "approaches) and nineteen educational interventions.\n\nCONCLUSIONS: Among the different " + + "strategies, the simulations are the most used. Despite this, our scoping review reveals " + + "that is necessary to use different teaching strategies to stimulate critical thinking, " + + "improve diagnostic reasoning, refine clinical judgment, and strengthen decision-making. " + + "However, it is not possible to demonstrate which methodology is more effective in obtaining " + + "the learning outcomes necessary to acquire an adequate level of judgment and critical " + + "thinking. Therefore, it will be necessary to relate teaching methodologies with the skills " + + "developed.\n\nAn unlabeled section of an abstract.\n\nAn abstract section with no attributes" + + " at all, concerning."); MetadatumDTO identifierOther = createMetadatumDTO("dc", "identifier", "other", "36708638"); MetadatumDTO author1 = createMetadatumDTO("dc", "contributor", "author", "Giuffrida, Silvia"); MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "Silano, Verdiana"); @@ -148,11 +147,6 @@ private ArrayList getRecords() { metadatums.add(title); metadatums.add(description1); - metadatums.add(description2); - metadatums.add(description3); - metadatums.add(description4); - metadatums.add(description5); - metadatums.add(description6); metadatums.add(identifierOther); metadatums.add(author1); metadatums.add(author2); @@ -210,4 +204,4 @@ private ArrayList getRecords2() { return records; } -} \ No newline at end of file +} diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test.xml b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test.xml index 666fb1e7d550..6af527246900 100644 --- a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test.xml +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test.xml @@ -41,6 +41,8 @@ The studies included and analyzed in this scoping review cover from January 2016 to June 2022. Primary studies and secondary revision studies, published in biomedical databases, were selected, including qualitative ones. Electronic databases used were: CINAHL, PubMed, Cochrane Library, Scopus, and OVID. Three authors independently evaluated the articles for titles, abstracts, and full text. 1433 articles were examined, applying the eligibility and exclusion criteria 73 studies were assessed for eligibility, and 27 were included in the scoping review. The results that emerged from the review were interpreted and grouped into three macro strategies (simulations-based education, art and visual thinking, and other learning approaches) and nineteen educational interventions. Among the different strategies, the simulations are the most used. Despite this, our scoping review reveals that is necessary to use different teaching strategies to stimulate critical thinking, improve diagnostic reasoning, refine clinical judgment, and strengthen decision-making. However, it is not possible to demonstrate which methodology is more effective in obtaining the learning outcomes necessary to acquire an adequate level of judgment and critical thinking. Therefore, it will be necessary to relate teaching methodologies with the skills developed. + An unlabeled section of an abstract. + An abstract section with no attributes at all, concerning. Copyright © 2023 Elsevier Ltd. All rights reserved. @@ -191,4 +193,4 @@ - \ No newline at end of file + diff --git a/dspace/config/spring/api/pubmed-integration.xml b/dspace/config/spring/api/pubmed-integration.xml index adec4456ea03..f488327789f9 100644 --- a/dspace/config/spring/api/pubmed-integration.xml +++ b/dspace/config/spring/api/pubmed-integration.xml @@ -57,7 +57,7 @@ - + From 2c400bf2da591bcb9d0978dfd60a44f0de04112e Mon Sep 17 00:00:00 2001 From: DSpace Bot <68393067+dspace-bot@users.noreply.github.com> Date: Wed, 11 Jun 2025 07:20:59 -0500 Subject: [PATCH 140/701] [Port dspace-7_x] Optimization of Solr Queries: Transition to Filter Queries (#10887) * use filter query instead of generic query (cherry picked from commit f2417feeca653aefdd7a201460afe3d9429bacae) * use filter query instead of generic query (cherry picked from commit d83a2525ad96e9d4cb935481c505537bdee0cae5) * use filter query instead of generic query (cherry picked from commit f3a976107e2afef5c2d0fb19e580bbbd754acc14) * remove obsolete comment (cherry picked from commit 3ee2dbcc56c157499c6db96cc3b8e90c2ae47e8f) * use filter query instead of generic query (cherry picked from commit 318afc769a6f0958c795da7506b25a28cf298f09) * add static imports (cherry picked from commit 8ad19c42df754daa6ed70c6e9d1c9146e72c0b32) * move static import to the top of the import block (cherry picked from commit b85585c34e528ce2165e25a0cf23351283360ac7) * move static imports to the top of the import block (cherry picked from commit 4b446e24a068027e8cf3c3935ad256f39b4b65ef) --------- Co-authored-by: Sascha Szott --- .../java/org/dspace/app/sitemap/GenerateSitemaps.java | 11 ++++++++--- .../app/solrdatabaseresync/SolrDatabaseResyncCli.java | 3 ++- .../main/java/org/dspace/browse/SolrBrowseDAO.java | 9 +++++++-- .../java/org/dspace/discovery/SolrSearchCore.java | 8 +++++--- .../org/dspace/statistics/SolrLoggerServiceImpl.java | 1 - 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java b/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java index 90962d12aa75..41b0b0f6b3dd 100644 --- a/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java +++ b/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java @@ -7,6 +7,8 @@ */ package org.dspace.app.sitemap; +import static org.dspace.discovery.SearchUtils.RESOURCE_TYPE_FIELD; + import java.io.File; import java.io.IOException; import java.sql.SQLException; @@ -189,7 +191,8 @@ public static void generateSitemaps(boolean makeHTMLMap, boolean makeSitemapOrg) try { DiscoverQuery discoveryQuery = new DiscoverQuery(); discoveryQuery.setMaxResults(PAGE_SIZE); - discoveryQuery.setQuery("search.resourcetype:Community"); + discoveryQuery.setQuery("*:*"); + discoveryQuery.addFilterQueries(RESOURCE_TYPE_FIELD + ":Community"); do { discoveryQuery.setStart(offset); DiscoverResult discoverResult = searchService.search(c, discoveryQuery); @@ -213,7 +216,8 @@ public static void generateSitemaps(boolean makeHTMLMap, boolean makeSitemapOrg) offset = 0; discoveryQuery = new DiscoverQuery(); discoveryQuery.setMaxResults(PAGE_SIZE); - discoveryQuery.setQuery("search.resourcetype:Collection"); + discoveryQuery.setQuery("*:*"); + discoveryQuery.addFilterQueries(RESOURCE_TYPE_FIELD + ":Collection"); do { discoveryQuery.setStart(offset); DiscoverResult discoverResult = searchService.search(c, discoveryQuery); @@ -237,7 +241,8 @@ public static void generateSitemaps(boolean makeHTMLMap, boolean makeSitemapOrg) offset = 0; discoveryQuery = new DiscoverQuery(); discoveryQuery.setMaxResults(PAGE_SIZE); - discoveryQuery.setQuery("search.resourcetype:Item"); + discoveryQuery.setQuery("*:*"); + discoveryQuery.addFilterQueries(RESOURCE_TYPE_FIELD + ":Item"); discoveryQuery.addSearchField("search.entitytype"); do { diff --git a/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCli.java b/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCli.java index aac42ce1acf9..0ce6b1a9ef3d 100644 --- a/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCli.java +++ b/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCli.java @@ -98,7 +98,8 @@ public void internalRun() throws Exception { private void performStatusUpdate(Context context) throws SearchServiceException, SolrServerException, IOException { SolrQuery solrQuery = new SolrQuery(); - solrQuery.setQuery(STATUS_FIELD + ":" + STATUS_FIELD_PREDB); + solrQuery.setQuery("*:*"); + solrQuery.addFilterQuery(STATUS_FIELD + ":" + STATUS_FIELD_PREDB); solrQuery.addFilterQuery(SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE); String dateRangeFilter = SearchUtils.LAST_INDEXED_FIELD + ":[* TO " + maxTime + "]"; logDebugAndOut("Date range filter used; " + dateRangeFilter); diff --git a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java index 14dab3e56174..a0a7725fa13a 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -7,6 +7,9 @@ */ package org.dspace.browse; +import static org.dspace.discovery.SearchUtils.RESOURCE_ID_FIELD; +import static org.dspace.discovery.SearchUtils.RESOURCE_TYPE_FIELD; + import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; @@ -308,8 +311,10 @@ public List doQuery() throws BrowseException { public String doMaxQuery(String column, String table, int itemID) throws BrowseException { DiscoverQuery query = new DiscoverQuery(); - query.setQuery("search.resourceid:" + itemID - + " AND search.resourcetype:" + IndexableItem.TYPE); + query.setQuery("*:*"); + query.addFilterQueries( + RESOURCE_ID_FIELD + ":" + itemID, + RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE); query.setMaxResults(1); DiscoverResult resp = null; try { diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrSearchCore.java b/dspace-api/src/main/java/org/dspace/discovery/SolrSearchCore.java index f31feab6123a..0d5dfad3b8b6 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrSearchCore.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrSearchCore.java @@ -88,9 +88,11 @@ protected void initSolr() { solrServer.setBaseURL(solrService); solrServer.setUseMultiPartPost(true); // Dummy/test query to search for Item (type=2) of ID=1 - SolrQuery solrQuery = new SolrQuery() - .setQuery(SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE + - " AND " + SearchUtils.RESOURCE_ID_FIELD + ":1"); + SolrQuery solrQuery = new SolrQuery(); + solrQuery.setQuery("*:*"); + solrQuery.addFilterQuery( + SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE, + SearchUtils.RESOURCE_ID_FIELD + ":1"); // Only return obj identifier fields in result doc solrQuery.setFields(SearchUtils.RESOURCE_TYPE_FIELD, SearchUtils.RESOURCE_ID_FIELD); solrServer.query(solrQuery, REQUEST_METHOD); diff --git a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java index 6c3ccdc082eb..35f86e3a1c0a 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/SolrLoggerServiceImpl.java @@ -1093,7 +1093,6 @@ public QueryResponse query(String query, String filterQuery, String facetField, return null; } - // System.out.println("QUERY"); SolrQuery solrQuery = new SolrQuery().setRows(rows).setQuery(query) .setFacetMinCount(facetMinCount); addAdditionalSolrYearCores(solrQuery); From 2abbe67ea6de8093f2223b8a44991618ec2fc934 Mon Sep 17 00:00:00 2001 From: DSpace Bot <68393067+dspace-bot@users.noreply.github.com> Date: Wed, 11 Jun 2025 07:26:49 -0500 Subject: [PATCH 141/701] [Port dspace-7_x] improve robustness of search in index field submit (use filter query) (#10890) * improve robustness of search in index field submit (use filter query) (cherry picked from commit a65ef008b74cac3a81c17bfd6ca44e095f6a3771) * fix checkstyle warnings (cherry picked from commit 183d5ca67113ea750a9de1a13413d34eac946169) * fix checkstyle warning (cherry picked from commit fe251f39e38dcdbba6ab3183a12fc5385c320b12) --------- Co-authored-by: Sascha Szott --- .../dspace/content/EntityTypeServiceImpl.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java index b5b066d9c36f..7df892cd56f5 100644 --- a/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java @@ -30,6 +30,7 @@ import org.dspace.core.Context; import org.dspace.discovery.SolrSearchCore; import org.dspace.discovery.indexobject.IndexableCollection; +import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; import org.springframework.beans.factory.annotation.Autowired; @@ -124,24 +125,33 @@ public void delete(Context context,EntityType entityType) throws SQLException, A public List getSubmitAuthorizedTypes(Context context) throws SQLException, SolrServerException, IOException { List types = new ArrayList<>(); - StringBuilder query = new StringBuilder(); - org.dspace.eperson.EPerson currentUser = context.getCurrentUser(); + StringBuilder query = null; + EPerson currentUser = context.getCurrentUser(); if (!authorizeService.isAdmin(context)) { String userId = ""; if (currentUser != null) { userId = currentUser.getID().toString(); + query = new StringBuilder(); + query.append("submit:(e").append(userId); } - query.append("submit:(e").append(userId); + Set groups = groupService.allMemberGroupsSet(context, currentUser); for (Group group : groups) { - query.append(" OR g").append(group.getID()); + if (query == null) { + query = new StringBuilder(); + query.append("submit:(g"); + } else { + query.append(" OR g"); + } + query.append(group.getID()); } query.append(")"); - } else { - query.append("*:*"); } - SolrQuery sQuery = new SolrQuery(query.toString()); + SolrQuery sQuery = new SolrQuery("*:*"); + if (query != null) { + sQuery.addFilterQuery(query.toString()); + } sQuery.addFilterQuery("search.resourcetype:" + IndexableCollection.TYPE); sQuery.setRows(0); sQuery.addFacetField("search.entitytype"); From d9cc564acec4a4d9add57ed52655745abaa4737c Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Sun, 13 Apr 2025 17:34:14 +0300 Subject: [PATCH 142/701] dspace-api: use static variable RESOURCE_TYPE_FIELD --- .../authorize/AuthorizeServiceImpl.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java index a99d83764a28..6bd4ed798dd1 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -9,6 +9,7 @@ import static org.dspace.app.util.AuthorizeUtil.canCollectionAdminManageAccounts; import static org.dspace.app.util.AuthorizeUtil.canCommunityAdminManageAccounts; +import static org.dspace.discovery.SearchUtils.RESOURCE_TYPE_FIELD; import java.sql.SQLException; import java.util.ArrayList; @@ -736,7 +737,7 @@ public List getPoliciesActionFilterExceptRpType(Context c, DSpac */ @Override public boolean isCommunityAdmin(Context context) throws SQLException { - return performCheck(context, "search.resourcetype:" + IndexableCommunity.TYPE); + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE); } /** @@ -749,7 +750,7 @@ public boolean isCommunityAdmin(Context context) throws SQLException { */ @Override public boolean isCollectionAdmin(Context context) throws SQLException { - return performCheck(context, "search.resourcetype:" + IndexableCollection.TYPE); + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE); } /** @@ -762,7 +763,7 @@ public boolean isCollectionAdmin(Context context) throws SQLException { */ @Override public boolean isItemAdmin(Context context) throws SQLException { - return performCheck(context, "search.resourcetype:" + IndexableItem.TYPE); + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE); } /** @@ -776,8 +777,8 @@ public boolean isItemAdmin(Context context) throws SQLException { @Override public boolean isComColAdmin(Context context) throws SQLException { return performCheck(context, - "(search.resourcetype:" + IndexableCommunity.TYPE + " OR search.resourcetype:" + - IndexableCollection.TYPE + ")"); + "(" + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE + " OR " + + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE + ")"); } /** @@ -795,7 +796,7 @@ public List findAdminAuthorizedCommunity(Context context, String quer throws SearchServiceException, SQLException { List communities = new ArrayList<>(); query = formatCustomQuery(query); - DiscoverResult discoverResult = getDiscoverResult(context, query + "search.resourcetype:" + + DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, offset, limit, null, null); for (IndexableObject solrCollections : discoverResult.getIndexableObjects()) { @@ -817,7 +818,7 @@ public List findAdminAuthorizedCommunity(Context context, String quer public long countAdminAuthorizedCommunity(Context context, String query) throws SearchServiceException, SQLException { query = formatCustomQuery(query); - DiscoverResult discoverResult = getDiscoverResult(context, query + "search.resourcetype:" + + DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, null, null, null, null); return discoverResult.getTotalSearchResults(); @@ -842,7 +843,7 @@ public List findAdminAuthorizedCollection(Context context, String qu } query = formatCustomQuery(query); - DiscoverResult discoverResult = getDiscoverResult(context, query + "search.resourcetype:" + + DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, offset, limit, CollectionService.SOLR_SORT_FIELD, SORT_ORDER.asc); for (IndexableObject solrCollections : discoverResult.getIndexableObjects()) { @@ -864,7 +865,7 @@ public List findAdminAuthorizedCollection(Context context, String qu public long countAdminAuthorizedCollection(Context context, String query) throws SearchServiceException, SQLException { query = formatCustomQuery(query); - DiscoverResult discoverResult = getDiscoverResult(context, query + "search.resourcetype:" + + DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, null, null, null, null); return discoverResult.getTotalSearchResults(); From bd753005e68dd3c3da0533a4a8aae9cc553e1adb Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Sun, 13 Apr 2025 17:35:55 +0300 Subject: [PATCH 143/701] dspace-api: do not request actual search hits in count-only query --- .../main/java/org/dspace/authorize/AuthorizeServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java index 6bd4ed798dd1..7198e28d03b9 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -820,7 +820,7 @@ public long countAdminAuthorizedCommunity(Context context, String query) query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, - null, null, null, null); + null, 0, null, null); return discoverResult.getTotalSearchResults(); } @@ -867,7 +867,7 @@ public long countAdminAuthorizedCollection(Context context, String query) query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, - null, null, null, null); + null, 0, null, null); return discoverResult.getTotalSearchResults(); } From 9eef166b7e90c7206221ad76d6e116c7da41b716 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Sun, 13 Apr 2025 17:36:35 +0300 Subject: [PATCH 144/701] dspace-api: set search fields in Solr query only if we are interested in the actual search results --- .../org/dspace/discovery/SolrServiceImpl.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index 8cf1ec85888c..06edf4408c07 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -861,16 +861,20 @@ protected SolrQuery resolveToSolrQuery(Context context, DiscoverQuery discoveryQ solrQuery.setQuery(query); - // Add any search fields to our query. This is the limited list - // of fields that will be returned in the solr result - for (String fieldName : discoveryQuery.getSearchFields()) { - solrQuery.addField(fieldName); - } - // Also ensure a few key obj identifier fields are returned with every query - solrQuery.addField(SearchUtils.RESOURCE_TYPE_FIELD); - solrQuery.addField(SearchUtils.RESOURCE_ID_FIELD); - solrQuery.addField(SearchUtils.RESOURCE_UNIQUE_ID); - solrQuery.addField(STATUS_FIELD); + if (discoveryQuery.getMaxResults() != 0) { + // set search fields in Solr query only if we are interested in the actual search results + + // Add any search fields to our query. This is the limited list + // of fields that will be returned in the solr result + for (String fieldName : discoveryQuery.getSearchFields()) { + solrQuery.addField(fieldName); + } + // Also ensure a few key obj identifier fields are returned with every query + solrQuery.addField(SearchUtils.RESOURCE_TYPE_FIELD); + solrQuery.addField(SearchUtils.RESOURCE_ID_FIELD); + solrQuery.addField(SearchUtils.RESOURCE_UNIQUE_ID); + solrQuery.addField(STATUS_FIELD); + } if (discoveryQuery.isSpellCheck()) { solrQuery.setParam(SpellingParams.SPELLCHECK_Q, query); From 34134b3c3b99779d07c8bda9181b9eeaacd2f6a3 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Wed, 9 Apr 2025 13:39:05 +0200 Subject: [PATCH 145/701] remove inclusion of sword-client.cfg --- dspace/config/dspace.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 4621ca51dc13..3047c2ce4db7 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1658,7 +1658,6 @@ include = ${module_dir}/solrauthority.cfg include = ${module_dir}/researcher-profile.cfg include = ${module_dir}/spring.cfg include = ${module_dir}/submission-curation.cfg -include = ${module_dir}/sword-client.cfg include = ${module_dir}/sword-server.cfg include = ${module_dir}/swordv2-server.cfg include = ${module_dir}/translator.cfg From 07e840b675ee933868ac53e41e1034b749f0e627 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Wed, 9 Apr 2025 13:40:02 +0200 Subject: [PATCH 146/701] removal of configuration file sword-client.cfg --- dspace/config/modules/sword-client.cfg | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 dspace/config/modules/sword-client.cfg diff --git a/dspace/config/modules/sword-client.cfg b/dspace/config/modules/sword-client.cfg deleted file mode 100644 index 9e8a029ae684..000000000000 --- a/dspace/config/modules/sword-client.cfg +++ /dev/null @@ -1,23 +0,0 @@ -#---------------------------------------------------------------# -#--------------SWORD V.1 CLIENT CONFIGURATIONS------------------# -#---------------------------------------------------------------# -# Configuration properties used solely by the UI-based SWORD # -# Client interface (used to submit DSpace content to another # -# SWORD server). # -#---------------------------------------------------------------# -# TODO: UNSUPPORTED in DSpace 7.0 -# List of remote Sword servers. Used to build the drop-down list of selectable Sword targets. -sword-client.targets = http://localhost:8080/sword/servicedocument, \ - http://client.swordapp.org/client/servicedocument, \ - http://dspace.swordapp.org/sword/servicedocument, \ - http://sword.eprints.org/sword-app/servicedocument, \ - http://sword.intralibrary.com/IntraLibrary-Deposit/service, \ - http://fedora.swordapp.org/sword-fedora/servicedocument - -# List of file types from which the user can select. If a type is not supported by the remote server -# it will not appear in the drop-down list. -sword-client.file-types = application/zip - -# List of package formats from which the user can select. If a format is not supported by the remote server -# it will not appear in the drop-down list. -sword-client.package-formats = http://purl.org/net/sword-types/METSDSpaceSIP From 39def525922f9bba8bc0a39e521f6ce7fbaa02cb Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Fri, 24 Jan 2025 01:21:25 +0100 Subject: [PATCH 147/701] [DURACOM-318] add new ITs for ResourcePolicy (cherry picked from commit d78d4f00d94ef0d4b147031b075b8df1e8896fe4) (cherry picked from commit 98c2b9942167890d24b71f5c32e6918ac2eee7a6) --- .../rest/ResourcePolicyRestRepositoryIT.java | 548 ++++++++++++++++++ 1 file changed, 548 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java index 5d2a05ab6440..08ce836f3d78 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java @@ -23,6 +23,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.text.SimpleDateFormat; +import java.io.InputStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; @@ -32,6 +35,9 @@ import javax.ws.rs.core.MediaType; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.ws.rs.core.MediaType; +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.matcher.ResourcePolicyMatcher; import org.dspace.app.rest.model.ResourcePolicyRest; @@ -43,12 +49,14 @@ import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EPersonBuilder; import org.dspace.builder.GroupBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.builder.ResourcePolicyBuilder; +import org.dspace.content.Bitstream; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; @@ -1215,6 +1223,376 @@ public void createOneForbiddenTest() throws Exception { .andExpect(jsonPath("$.page.totalElements", is(0))); } + @Test + public void createPolicyByCollectionAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson colAdmin = EPersonBuilder.createEPerson(context) + .withEmail("colAdmin@mail.test") + .withPassword(password) + .build(); + + EPerson colAdmin2 = EPersonBuilder.createEPerson(context) + .withEmail("colAdmin2@mail.test") + .withPassword(password) + .build(); + + EPerson submitter = EPersonBuilder.createEPerson(context) + .withEmail("colSubmitter@mail.test") + .withPassword(password) + .build(); + + Community community = CommunityBuilder.createCommunity(context) + .withName("My top commynity") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("My collection") + .withAdminGroup(colAdmin) + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + CollectionBuilder.createCollection(context, community) + .withName("My Second Collection") + .withAdminGroup(colAdmin2) + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + Item publication = ItemBuilder.createItem(context, collection) + .withTitle("Public item") + .build(); + + //Add a bitstream to a publication + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream("ThisIsSomeDummyText", CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder.createBitstream(context, publication, is) + .withName("Bitstream") + .withDescription("description") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + ResourcePolicyRest resourcePolicyRest = new ResourcePolicyRest(); + resourcePolicyRest.setPolicyType(ResourcePolicy.TYPE_CUSTOM); + resourcePolicyRest.setAction(Constants.actionText[Constants.WRITE]); + resourcePolicyRest.setName("Test for collection admin"); + + String authcolAdminToken = getAuthToken(colAdmin.getEmail(), password); + String authcolAdmin2Token = getAuthToken(colAdmin2.getEmail(), password); + String authSubmitterToken = getAuthToken(submitter.getEmail(), password); + AtomicReference idRef = new AtomicReference(); + + try { + // submitter can't create policy + getClient(authSubmitterToken).perform(post("/api/authz/resourcepolicies") + .content(new ObjectMapper().writeValueAsBytes(resourcePolicyRest)) + .param("resource", bitstream.getID().toString()) + .param("eperson", submitter.getID().toString()) + .param("projections", "full") + .contentType(contentType)) + .andExpect(status().isForbidden()); + + // other collection admin can't create policy for other collection + getClient(authcolAdmin2Token).perform(post("/api/authz/resourcepolicies") + .content(new ObjectMapper().writeValueAsBytes(resourcePolicyRest)) + .param("resource", bitstream.getID().toString()) + .param("eperson", submitter.getID().toString()) + .param("projections", "full") + .contentType(contentType)) + .andExpect(status().isForbidden()); + + // create policy for submitter by collection admin + getClient(authcolAdminToken).perform(post("/api/authz/resourcepolicies") + .content(new ObjectMapper().writeValueAsBytes(resourcePolicyRest)) + .param("resource", bitstream.getID().toString()) + .param("eperson", submitter.getID().toString()) + .param("projections", "full") + .contentType(contentType)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", ResourcePolicyMatcher.matchFullEmbeds())) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.name", is(resourcePolicyRest.getName())), + hasJsonPath("$.description", is(resourcePolicyRest.getDescription())), + hasJsonPath("$.policyType", is(resourcePolicyRest.getPolicyType())), + hasJsonPath("$.action", is(resourcePolicyRest.getAction())), + hasJsonPath("$.startDate", is(resourcePolicyRest.getStartDate())), + hasJsonPath("$.endDate", is(resourcePolicyRest.getEndDate())), + hasJsonPath("$.type", is(resourcePolicyRest.getType()))))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + // submitter can see own policy + getClient(authSubmitterToken).perform(get("/api/authz/resourcepolicies/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + idRef.get()))); + + // collection admin can see that policy + getClient(authcolAdminToken).perform(get("/api/authz/resourcepolicies/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + idRef.get()))); + } finally { + ResourcePolicyBuilder.delete(idRef.get()); + } + } + + @Test + public void createPolicyBySubCommunityAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson comAdmin = EPersonBuilder.createEPerson(context) + .withEmail("comAdmin@mail.test") + .withPassword(password) + .build(); + + EPerson comAdmin2 = EPersonBuilder.createEPerson(context) + .withEmail("comAdmin2@mail.test") + .withPassword(password) + .build(); + + EPerson submitter = EPersonBuilder.createEPerson(context) + .withEmail("colSubmitter@mail.test") + .withPassword(password) + .build(); + + Community community = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("My First Commynity") + .withAdminGroup(comAdmin) + .build(); + + Community community2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("My Second Commynity") + .withAdminGroup(comAdmin2) + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("My collection") + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + CollectionBuilder.createCollection(context, community2) + .withName("My Second Collection") + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + Item publication = ItemBuilder.createItem(context, collection) + .withTitle("Public item") + .build(); + + context.restoreAuthSystemState(); + + ResourcePolicyRest resourcePolicyRest = new ResourcePolicyRest(); + resourcePolicyRest.setPolicyType(ResourcePolicy.TYPE_CUSTOM); + resourcePolicyRest.setAction(Constants.actionText[Constants.WRITE]); + resourcePolicyRest.setName("Test for collection admin"); + + String authcomAdminToken = getAuthToken(comAdmin.getEmail(), password); + String authcomAdmin2Token = getAuthToken(comAdmin2.getEmail(), password); + String authSubmitterToken = getAuthToken(submitter.getEmail(), password); + AtomicReference idRef = new AtomicReference(); + + try { + // submitter can't create policy + getClient(authSubmitterToken).perform(post("/api/authz/resourcepolicies") + .content(new ObjectMapper().writeValueAsBytes(resourcePolicyRest)) + .param("resource", publication.getID().toString()) + .param("eperson", submitter.getID().toString()) + .param("projections", "full") + .contentType(contentType)) + .andExpect(status().isForbidden()); + + // other Community admin can't create policy for collections into other Community + getClient(authcomAdmin2Token).perform(post("/api/authz/resourcepolicies") + .content(new ObjectMapper().writeValueAsBytes(resourcePolicyRest)) + .param("resource", publication.getID().toString()) + .param("eperson", submitter.getID().toString()) + .param("projections", "full") + .contentType(contentType)) + .andExpect(status().isForbidden()); + + // create policy for submitter by Community admin + getClient(authcomAdminToken).perform(post("/api/authz/resourcepolicies") + .content(new ObjectMapper().writeValueAsBytes(resourcePolicyRest)) + .param("resource", publication.getID().toString()) + .param("eperson", submitter.getID().toString()) + .param("projections", "full") + .contentType(contentType)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", ResourcePolicyMatcher.matchFullEmbeds())) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.name", is(resourcePolicyRest.getName())), + hasJsonPath("$.description", is(resourcePolicyRest.getDescription())), + hasJsonPath("$.policyType", is(resourcePolicyRest.getPolicyType())), + hasJsonPath("$.action", is(resourcePolicyRest.getAction())), + hasJsonPath("$.startDate", is(resourcePolicyRest.getStartDate())), + hasJsonPath("$.endDate", is(resourcePolicyRest.getEndDate())), + hasJsonPath("$.type", is(resourcePolicyRest.getType()))))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + // submitter can see own policy + getClient(authSubmitterToken).perform(get("/api/authz/resourcepolicies/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + idRef.get()))); + + // community admin can see policies of own collections/items + getClient(authcomAdminToken).perform(get("/api/authz/resourcepolicies/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + idRef.get()))); + + // Other community admin can't see policies of other community's collections/items + getClient(authcomAdmin2Token).perform(get("/api/authz/resourcepolicies/" + idRef.get())) + .andExpect(status().isForbidden()); + } finally { + ResourcePolicyBuilder.delete(idRef.get()); + } + } + + @Test + public void createPolicyByCommunityAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson rootComAdmin = EPersonBuilder.createEPerson(context) + .withEmail("rootComAdmin@mail.test") + .withPassword(password) + .build(); + + EPerson submitter = EPersonBuilder.createEPerson(context) + .withEmail("colSubmitter@mail.test") + .withPassword(password) + .build(); + + Community rootCommunity = CommunityBuilder.createCommunity(context) + .withName("Root Community") + .withAdminGroup(rootComAdmin) + .build(); + + Community community = CommunityBuilder.createSubCommunity(context, rootCommunity) + .withName("My First Commynity") + .build(); + + Community community2 = CommunityBuilder.createSubCommunity(context, rootCommunity) + .withName("My Second Commynity") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("My collection") + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + CollectionBuilder.createCollection(context, community2) + .withName("My Second Collection") + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + Item publication = ItemBuilder.createItem(context, collection) + .withTitle("Public item") + .build(); + + Collection collection2 = CollectionBuilder.createCollection(context, community) + .withName("My Second Collection") + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + Item publication2 = ItemBuilder.createItem(context, collection2) + .withTitle("Item of second collection") + .build(); + + //Add a bitstream to a publication + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream("ThisIsSomeDummyText", CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder.createBitstream(context, publication2, is) + .withName("Bitstream") + .withDescription("description") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + ResourcePolicyRest resourcePolicyRest = new ResourcePolicyRest(); + resourcePolicyRest.setPolicyType(ResourcePolicy.TYPE_CUSTOM); + resourcePolicyRest.setAction(Constants.actionText[Constants.WRITE]); + resourcePolicyRest.setName("Test for collection admin"); + + ResourcePolicyRest resourcePolicyRest2 = new ResourcePolicyRest(); + resourcePolicyRest2.setPolicyType(ResourcePolicy.TYPE_CUSTOM); + resourcePolicyRest2.setAction(Constants.actionText[Constants.WRITE]); + resourcePolicyRest2.setName("Test for root community admin"); + + String authSubmitterToken = getAuthToken(submitter.getEmail(), password); + String authRootAdminToken = getAuthToken(rootComAdmin.getEmail(), password); + + AtomicReference idRef = new AtomicReference(); + AtomicReference idRef2 = new AtomicReference(); + try { + // create policy for submitter by root Community admin + getClient(authRootAdminToken).perform(post("/api/authz/resourcepolicies") + .content(new ObjectMapper().writeValueAsBytes(resourcePolicyRest)) + .param("resource", publication.getID().toString()) + .param("eperson", submitter.getID().toString()) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", ResourcePolicyMatcher.matchFullEmbeds())) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.name", is(resourcePolicyRest.getName())), + hasJsonPath("$.description", is(resourcePolicyRest.getDescription())), + hasJsonPath("$.policyType", is(resourcePolicyRest.getPolicyType())), + hasJsonPath("$.action", is(resourcePolicyRest.getAction())), + hasJsonPath("$.startDate", is(resourcePolicyRest.getStartDate())), + hasJsonPath("$.endDate", is(resourcePolicyRest.getEndDate())), + hasJsonPath("$.type", is(resourcePolicyRest.getType()))))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + // create policy for submitter by root Community admin + getClient(authRootAdminToken).perform(post("/api/authz/resourcepolicies") + .content(new ObjectMapper().writeValueAsBytes(resourcePolicyRest)) + .param("resource", bitstream.getID().toString()) + .param("eperson", submitter.getID().toString()) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", ResourcePolicyMatcher.matchFullEmbeds())) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.name", is(resourcePolicyRest.getName())), + hasJsonPath("$.description", is(resourcePolicyRest.getDescription())), + hasJsonPath("$.policyType", is(resourcePolicyRest.getPolicyType())), + hasJsonPath("$.action", is(resourcePolicyRest.getAction())), + hasJsonPath("$.startDate", is(resourcePolicyRest.getStartDate())), + hasJsonPath("$.endDate", is(resourcePolicyRest.getEndDate())), + hasJsonPath("$.type", is(resourcePolicyRest.getType()))))) + .andDo(result -> idRef2.set(read(result.getResponse().getContentAsString(), "$.id"))); + + getClient(authSubmitterToken).perform(get("/api/authz/resourcepolicies/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + idRef.get()))); + + getClient(authSubmitterToken).perform(get("/api/authz/resourcepolicies/" + idRef2.get())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + idRef2.get()))); + } finally { + ResourcePolicyBuilder.delete(idRef.get()); + ResourcePolicyBuilder.delete(idRef2.get()); + } + } + @Test public void deleteOne() throws Exception { context.turnOffAuthorisationSystem(); @@ -1308,6 +1686,176 @@ public void deleteOneNotFoundTest() throws Exception { .andExpect(status().isNotFound()); } + @Test + public void deletePolicyByCollectionAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson colAdmin = EPersonBuilder.createEPerson(context) + .withEmail("colAdmin@mail.test") + .withPassword(password) + .build(); + + EPerson colAdmin2 = EPersonBuilder.createEPerson(context) + .withEmail("colAdmin2@mail.test") + .withPassword(password) + .build(); + + EPerson submitter = EPersonBuilder.createEPerson(context) + .withEmail("colSubmitter@mail.test") + .withPassword(password) + .build(); + + Community community = CommunityBuilder.createCommunity(context) + .withName("My top commynity") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("My collection") + .withAdminGroup(colAdmin) + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + CollectionBuilder.createCollection(context, community) + .withName("My Second Collection") + .withAdminGroup(colAdmin2) + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + Item publication = ItemBuilder.createItem(context, collection) + .withTitle("Public item") + .build(); + + //Add a bitstream to a publication + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream("ThisIsSomeDummyText", CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder.createBitstream(context, publication, is) + .withName("Bitstream") + .withDescription("description") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String authcolAdminToken = getAuthToken(colAdmin.getEmail(), password); + String authcolAdmin2Token = getAuthToken(colAdmin2.getEmail(), password); + String authSubmitterToken = getAuthToken(submitter.getEmail(), password); + + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withDspaceObject(bitstream) + .withAction(Constants.READ) + .withPolicyType(ResourcePolicy.TYPE_CUSTOM) + .withUser(submitter) + .build(); + + // submitter can't delete own policy + getClient(authSubmitterToken).perform(delete("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isForbidden()); + + // check that policy wasn't deleted + getClient(adminToken).perform(get("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + rp.getID()))); + + // other collection admin can't delete policy that belong to items of other collections + getClient(authcolAdmin2Token).perform(delete("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isForbidden()); + + // check that policy wasn't deleted + getClient(adminToken).perform(get("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + rp.getID()))); + + // delete policy for submitter by collection admin + getClient(authcolAdminToken).perform(delete("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isNoContent()); + + getClient(adminToken).perform(get("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void deletePolicyBySubCommunityAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson comAdmin = EPersonBuilder.createEPerson(context) + .withEmail("comAdmin@mail.test") + .withPassword(password) + .build(); + + EPerson comAdmin2 = EPersonBuilder.createEPerson(context) + .withEmail("comAdmin2@mail.test") + .withPassword(password) + .build(); + + EPerson submitter = EPersonBuilder.createEPerson(context) + .withEmail("colSubmitter@mail.test") + .withPassword(password) + .build(); + + Community community = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("My First Commynity") + .withAdminGroup(comAdmin) + .build(); + + Community community2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("My Second Commynity") + .withAdminGroup(comAdmin2) + .build(); + + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("My collection") + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + CollectionBuilder.createCollection(context, community2) + .withName("My Second Collection") + .withSubmitterGroup(submitter) + .withEntityType("Publication") + .build(); + + Item publication = ItemBuilder.createItem(context, collection) + .withTitle("Public item") + .build(); + + context.restoreAuthSystemState(); + + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + .withDspaceObject(publication) + .withAction(Constants.WRITE) + .withPolicyType(ResourcePolicy.TYPE_CUSTOM) + .withUser(submitter) + .build(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String authcomAdminToken = getAuthToken(comAdmin.getEmail(), password); + String authcomAdmin2Token = getAuthToken(comAdmin2.getEmail(), password); + + // other Community admin can't delete policy of other Community + getClient(authcomAdmin2Token).perform(delete("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isForbidden()); + + getClient(adminToken).perform(get("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/authz/resourcepolicies/" + rp.getID()))); + + // Community admin can delete policy + getClient(authcomAdminToken).perform(delete("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isNoContent()); + + // submitter can see own policy + getClient(adminToken).perform(get("/api/authz/resourcepolicies/" + rp.getID())) + .andExpect(status().isNotFound()); + } + @Test public void patchReplaceStartDateTest() throws Exception { context.turnOffAuthorisationSystem(); From f92e376896eabb56046951b6ddfbf12ba93b3753 Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Fri, 24 Jan 2025 02:03:23 +0100 Subject: [PATCH 148/701] [DURACOM-318] update security annotations on ResourcePolicyRepository (cherry picked from commit fabcc692db68e5232986ff062e849e4ec5c68c8b) (cherry picked from commit 95836c271cb4af41d3c29f2dda118eab4674f653) --- .../ResourcePolicyRestRepository.java | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java index a79a9fe4eaa4..c0341f15e346 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java @@ -25,6 +25,7 @@ import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.rest.repository.patch.ResourcePatch; +import org.dspace.app.rest.security.DSpacePermissionEvaluator; import org.dspace.app.rest.utils.DSpaceObjectUtils; import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.AuthorizeException; @@ -44,6 +45,8 @@ import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.hateoas.Link; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; /** @@ -73,6 +76,9 @@ public class ResourcePolicyRestRepository extends DSpaceRestRepository resourcePatch; + @Autowired + private DSpacePermissionEvaluator permissionEvaluator; + @Autowired DiscoverableEndpointsService discoverableEndpointsService; @@ -222,14 +228,13 @@ public Page findByGroup(@Parameter(value = "uuid", required } @Override - @PreAuthorize("hasAuthority('ADMIN')") + @PreAuthorize("isAuthenticated()") protected ResourcePolicyRest createAndReturn(Context context) throws AuthorizeException, SQLException { String resourceUuidStr = getRequestService().getCurrentRequest().getServletRequest().getParameter("resource"); String epersonUuidStr = getRequestService().getCurrentRequest().getServletRequest().getParameter("eperson"); String groupUuidStr = getRequestService().getCurrentRequest().getServletRequest().getParameter("group"); - if (resourceUuidStr == null) { throw new MissingParameterException("Missing resource (uuid) parameter"); } @@ -244,6 +249,11 @@ protected ResourcePolicyRest createAndReturn(Context context) throws AuthorizeEx UUID resourceUuid = UUID.fromString(resourceUuidStr); + if (isNotAuthorized(resourceUuid, "WRITE")) { + throw new AuthorizeException( + "User unauthorized to create a new ResourcePolicy for resource: " + resourceUuid); + } + try { resourcePolicyRest = mapper.readValue(req.getInputStream(), ResourcePolicyRest.class); } catch (IOException exIO) { @@ -298,7 +308,7 @@ protected ResourcePolicyRest createAndReturn(Context context) throws AuthorizeEx } @Override - @PreAuthorize("hasAuthority('ADMIN')") + @PreAuthorize("hasPermission(#id, 'resourcepolicy', 'ADMIN')") protected void delete(Context context, Integer id) throws AuthorizeException { ResourcePolicy resourcePolicy = null; try { @@ -332,4 +342,10 @@ public void afterPropertiesSet() throws Exception { Link.of("/api/" + ResourcePolicyRest.CATEGORY + "/" + ResourcePolicyRest.PLURAL_NAME + "/search", ResourcePolicyRest.PLURAL_NAME + "-search"))); } + + private boolean isNotAuthorized(UUID id, String permission) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return !permissionEvaluator.hasPermission(authentication, id, "resourcepolicy", permission); + } + } From de5908725af8bc991440506d5af985d04ac5f87c Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Fri, 24 Jan 2025 02:06:16 +0100 Subject: [PATCH 149/701] [DURACOM-318] improve sucurity plugin (cherry picked from commit b1ce88925ea36e84a77e667a94ae5577b5ee05b6) (cherry picked from commit e9be8435ec9fffec790ad965c162f89e11fedf97) --- .../authorize/ResourcePolicyServiceImpl.java | 6 ++- .../src/main/java/org/dspace/core/Utils.java | 43 +++++++++++++++++++ ...PolicyAdminPermissionEvalutatorPlugin.java | 42 +++++++++++++----- ...cePolicyRestPermissionEvaluatorPlugin.java | 1 - 4 files changed, 80 insertions(+), 12 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicyServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicyServiceImpl.java index 86998a2196e7..08a8a1463c03 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicyServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/ResourcePolicyServiceImpl.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.ObjectUtils; import org.apache.logging.log4j.Logger; import org.dspace.authorize.dao.ResourcePolicyDAO; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; @@ -51,6 +52,9 @@ public class ResourcePolicyServiceImpl implements ResourcePolicyService { @Autowired private GroupService groupService; + @Autowired + private AuthorizeService authorizeService; + protected ResourcePolicyServiceImpl() { } @@ -422,6 +426,6 @@ public boolean isMyResourcePolicy(Context context, EPerson eperson, Integer id) } else if (group != null && groupService.isMember(context, eperson, group)) { isMy = true; } - return isMy; + return isMy || authorizeService.isAdmin(context, eperson, resourcePolicy.getdSpaceObject()); } } diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index ea9ed57eca04..90df7240503e 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -506,4 +506,47 @@ public static String interpolateConfigsInString(String string) { ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService(); return StringSubstitutor.replace(string, config.getProperties()); } + + /** + * Get the maximum timestamp that can be stored in a PostgreSQL database with hibernate, + * for our "distant future" access expiry date. + * @return the maximum timestamp that can be stored with Postgres + Hibernate + */ + public static Instant getMaxTimestamp() { + return LocalDateTime.of(294276, 12, 31, 23, 59, 59) + .toInstant(ZoneOffset.UTC); + } + + /** + * Get the minimum timestamp that can be stored in a PostgreSQL database, for date validation or any other + * purpose to ensure we don't try to store a date before the epoch. + * @return the minimum timestamp that can be stored with Postgres + Hibernate + */ + public static Instant getMinTimestamp() { + return LocalDateTime.of(-4713, 11, 12, 0, 0, 0) + .toInstant(ZoneOffset.UTC); + } + + /** + * Checks whether a given string can be converted to a valid {@code int} value. + *

+ * This method returns {@code false} if the input string is {@code null}, empty, + * or contains only whitespace. Otherwise, it attempts to parse the string as an + * integer using {@link Integer#parseInt(String)}. + * + * @param str the string to check for integer convertibility + * @return {@code true} if the string is non-blank and can be parsed as an integer; + * {@code false} otherwise + */ + public static boolean isConvertibleToInt(String str) { + if (StringUtils.isBlank(str)) { + return false; + } + try { + Integer.parseInt(str); + return true; + } catch (NumberFormatException e) { + return false; + } + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java index 421d25f9406d..69188b6cca5f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java @@ -9,15 +9,18 @@ import java.io.Serializable; import java.sql.SQLException; +import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.util.factory.UtilServiceFactory; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; +import org.dspace.core.Utils; import org.dspace.services.RequestService; import org.dspace.services.model.Request; import org.slf4j.Logger; @@ -38,7 +41,7 @@ public class ResourcePolicyAdminPermissionEvalutatorPlugin extends RestObjectPer private static final Logger log = LoggerFactory.getLogger(ResourcePolicyRestPermissionEvaluatorPlugin.class); - public static final String RESOURCE_POLICY_PATCH = "resourcepolicy"; + public static final String RESOURCE_POLICY_TYPE = "resourcepolicy"; @Autowired AuthorizeService authorizeService; @@ -55,8 +58,9 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t DSpaceRestPermission restPermission = DSpaceRestPermission.convert(permission); - if (!DSpaceRestPermission.ADMIN.equals(restPermission) - || !StringUtils.equalsIgnoreCase(targetType, RESOURCE_POLICY_PATCH)) { + if (!DSpaceRestPermission.ADMIN.equals(restPermission) && + !DSpaceRestPermission.WRITE.equals(restPermission) || + !StringUtils.equalsIgnoreCase(targetType, RESOURCE_POLICY_TYPE)) { return false; } @@ -64,19 +68,37 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t Context context = ContextUtil.obtainContext(request.getHttpServletRequest()); try { - int resourcePolicyID = Integer.parseInt(targetId.toString()); - ResourcePolicy resourcePolicy = resourcePolicyService.find(context, resourcePolicyID); - if (resourcePolicy == null) { - throw new ResourceNotFoundException( - ResourcePolicyRest.CATEGORY + "." + ResourcePolicyRest.NAME + - " with id: " + resourcePolicyID + " not found"); + DSpaceObject dso = null; + if (Utils.isConvertibleToInt(targetId.toString())) { + var id = Integer.parseInt(targetId.toString()); + dso = getDSO(context, id); + } else { + var uuid = UUID.fromString(targetId.toString()); + dso = getDSO(context, uuid); } - DSpaceObject dso = resourcePolicy.getdSpaceObject(); return authorizeService.isAdmin(context, dso); + } catch (SQLException e) { log.error(e.getMessage(), e); } return false; } + private DSpaceObject getDSO(Context context, int id) throws SQLException { + ResourcePolicy resourcePolicy = resourcePolicyService.find(context, id); + if (resourcePolicy == null) { + throw new ResourceNotFoundException( + ResourcePolicyRest.CATEGORY + "." + ResourcePolicyRest.NAME + " with id: " + id + " not found"); + } + return resourcePolicy.getdSpaceObject(); + } + + private DSpaceObject getDSO(Context context, UUID uuid) throws SQLException { + DSpaceObject dso = UtilServiceFactory.getInstance().getDSpaceObjectUtils().findDSpaceObject(context, uuid); + if (dso == null) { + throw new ResourceNotFoundException("DSpaceObject with uuid: " + uuid + " not found"); + } + return dso; + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java index bf7ce3b53f1a..5728fb866729 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java @@ -55,7 +55,6 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t DSpaceRestPermission restPermission = DSpaceRestPermission.convert(permission); if (!DSpaceRestPermission.READ.equals(restPermission) - && !DSpaceRestPermission.WRITE.equals(restPermission) && !DSpaceRestPermission.DELETE.equals(restPermission) || !StringUtils.equalsIgnoreCase(targetType, ResourcePolicyRest.NAME)) { return false; From a24340a19725610a77b0fba964edccaae02e9852 Mon Sep 17 00:00:00 2001 From: Adamo Date: Fri, 2 May 2025 09:38:28 +0200 Subject: [PATCH 150/701] [DURACOM-318] IT fix (cherry picked from commit accba0738f7684e31af707bf5bd06508e5571621) --- .../dspace/app/rest/ResourcePolicyRestRepositoryIT.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java index 08ce836f3d78..2e5998b999b2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java @@ -191,7 +191,7 @@ public void findOneUnAuthenticatedTest() throws Exception { public void findOneNotFoundTest() throws Exception { String authToken = getAuthToken(admin.getEmail(), password); - getClient(authToken).perform(get("/api/authz/resourcepolicies/" + UUID.randomUUID().toString())) + getClient(authToken).perform(get("/api/authz/resourcepolicies/" + UUID.randomUUID())) .andExpect(status().isNotFound()); } @@ -1743,11 +1743,10 @@ public void deletePolicyByCollectionAdminTest() throws Exception { String authcolAdmin2Token = getAuthToken(colAdmin2.getEmail(), password); String authSubmitterToken = getAuthToken(submitter.getEmail(), password); - ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context, submitter, null) .withDspaceObject(bitstream) .withAction(Constants.READ) .withPolicyType(ResourcePolicy.TYPE_CUSTOM) - .withUser(submitter) .build(); // submitter can't delete own policy @@ -1826,11 +1825,10 @@ public void deletePolicyBySubCommunityAdminTest() throws Exception { context.restoreAuthSystemState(); - ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context) + ResourcePolicy rp = ResourcePolicyBuilder.createResourcePolicy(context, submitter, null) .withDspaceObject(publication) .withAction(Constants.WRITE) .withPolicyType(ResourcePolicy.TYPE_CUSTOM) - .withUser(submitter) .build(); String adminToken = getAuthToken(admin.getEmail(), password); From 2104d605bd82d1372bb6af468dd8ee3bf210bb77 Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Mon, 3 Feb 2025 19:30:16 +0100 Subject: [PATCH 151/701] [DURACOM-318] improve code (cherry picked from commit 8e0ca2e6f88b0251edf8a840f65135590c65f088) (cherry picked from commit 4270170d40833b7cd4c505a850c47f4bdff55a1c) --- dspace-api/src/main/java/org/dspace/core/Utils.java | 1 + .../ResourcePolicyAdminPermissionEvalutatorPlugin.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index 90df7240503e..59d37f71cf42 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -549,4 +549,5 @@ public static boolean isConvertibleToInt(String str) { return false; } } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java index 69188b6cca5f..280946e64767 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java @@ -11,6 +11,7 @@ import java.sql.SQLException; import java.util.UUID; +import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.utils.ContextUtil; @@ -69,7 +70,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t try { DSpaceObject dso = null; - if (Utils.isConvertibleToInt(targetId.toString())) { + if (NumberUtils.isNumber(targetId.toString())) { var id = Integer.parseInt(targetId.toString()); dso = getDSO(context, id); } else { From 03992be08d3756e400a2740597d5f7d8d04cf8ad Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Mon, 3 Feb 2025 22:54:52 +0100 Subject: [PATCH 152/701] [DURACOM-318] remove unused import (cherry picked from commit ed91462ccd7f99f5cf0dc326ed06ab42c13b80e0) (cherry picked from commit 8df4e35e76364bf20dad51073e33cda8f3c3f623) --- .../security/ResourcePolicyAdminPermissionEvalutatorPlugin.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java index 280946e64767..e544665e434a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java @@ -21,7 +21,6 @@ import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; -import org.dspace.core.Utils; import org.dspace.services.RequestService; import org.dspace.services.model.Request; import org.slf4j.Logger; From 1732285d59fc143b1a9b8f112ef5292f861016fc Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Mon, 23 Jun 2025 10:12:56 +0200 Subject: [PATCH 153/701] add import for Intstant --- dspace-api/src/main/java/org/dspace/core/Utils.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index 59d37f71cf42..df1e0218a1f8 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -24,6 +24,7 @@ import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; From e90f792869b20eab9b59d985c4f8be0937c98c35 Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Mon, 23 Jun 2025 11:36:52 +0200 Subject: [PATCH 154/701] add missing imports --- dspace-api/src/main/java/org/dspace/core/Utils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index df1e0218a1f8..b13eb3ba360c 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -25,6 +25,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; From 61c6e59a060c63189974949651861a64279e9ff0 Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Mon, 23 Jun 2025 11:44:22 +0200 Subject: [PATCH 155/701] remove jakarta import --- .../java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java index 2e5998b999b2..61e86310c340 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java @@ -35,7 +35,6 @@ import javax.ws.rs.core.MediaType; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.ws.rs.core.MediaType; import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; From 8a84cba371af1370f2b21e53b923733feac88c57 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 23 Jun 2025 15:13:51 -0500 Subject: [PATCH 156/701] Fix broken ITs by removing unnecessary registrations and managing context permissions better --- .../VersionedHandleIdentifierProviderIT.java | 14 +++++-- ...ntifierProviderWithCanonicalHandlesIT.java | 38 +++++++++---------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java b/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java index fd6340fc6381..58ebd7866f82 100644 --- a/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java +++ b/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderIT.java @@ -48,15 +48,21 @@ public void setUp() throws Exception { collection = CollectionBuilder.createCollection(context, parentCommunity) .withName("Collection") .build(); + + context.restoreAuthSystemState(); } private void createVersions() throws SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + itemV1 = ItemBuilder.createItem(context, collection) .withTitle("First version") .build(); firstHandle = itemV1.getHandle(); itemV2 = VersionBuilder.createVersion(context, itemV1, "Second version").build().getItem(); itemV3 = VersionBuilder.createVersion(context, itemV1, "Third version").build().getItem(); + + context.restoreAuthSystemState(); } @Test @@ -76,8 +82,7 @@ public void testDefaultVersionedHandleProvider() throws Exception { @Test public void testCollectionHandleMetadata() { - registerProvider(VersionedHandleIdentifierProvider.class); - + context.turnOffAuthorisationSystem(); Community testCommunity = CommunityBuilder.createCommunity(context) .withName("Test community") .build(); @@ -85,6 +90,7 @@ public void testCollectionHandleMetadata() { Collection testCollection = CollectionBuilder.createCollection(context, testCommunity) .withName("Test Collection") .build(); + context.restoreAuthSystemState(); List metadata = ContentServiceFactory.getInstance().getDSpaceObjectService(testCollection) .getMetadata(testCollection, "dc", "identifier", "uri", @@ -96,11 +102,11 @@ public void testCollectionHandleMetadata() { @Test public void testCommunityHandleMetadata() { - registerProvider(VersionedHandleIdentifierProvider.class); - + context.turnOffAuthorisationSystem(); Community testCommunity = CommunityBuilder.createCommunity(context) .withName("Test community") .build(); + context.restoreAuthSystemState(); List metadata = ContentServiceFactory.getInstance().getDSpaceObjectService(testCommunity) .getMetadata(testCommunity, "dc", "identifier", "uri", diff --git a/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandlesIT.java b/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandlesIT.java index b4f2bc9b207f..fde52f42de47 100644 --- a/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandlesIT.java +++ b/dspace-api/src/test/java/org/dspace/identifier/VersionedHandleIdentifierProviderWithCanonicalHandlesIT.java @@ -14,7 +14,6 @@ import java.util.ArrayList; import java.util.List; -import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.authorize.AuthorizeException; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; @@ -27,10 +26,11 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.kernel.ServiceManager; import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.After; import org.junit.Before; import org.junit.Test; -public class VersionedHandleIdentifierProviderWithCanonicalHandlesIT extends AbstractIntegrationTestWithDatabase { +public class VersionedHandleIdentifierProviderWithCanonicalHandlesIT extends AbstractIdentifierProviderIT { private ServiceManager serviceManager; private IdentifierServiceImpl identifierService; @@ -62,34 +62,34 @@ public void setUp() throws Exception { .withName("Collection") .build(); - + registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); } - private void registerProvider(Class type) { - // Register our new provider - List servicesByType = serviceManager.getServicesByType(type); - if (servicesByType.isEmpty()) { - serviceManager.registerServiceClass(type.getName(), type); - } - IdentifierProvider identifierProvider = - (IdentifierProvider) serviceManager.getServiceByName(type.getName(), type); - - // Overwrite the identifier-service's providers with the new one to ensure only this provider is used - identifierService.setProviders(List.of(identifierProvider)); + @After + @Override + public void destroy() throws Exception { + super.destroy(); + // Unregister this non-default provider + unregisterProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); + // Re-register the default provider (for later tests) + registerProvider(VersionedHandleIdentifierProvider.class); } private void createVersions() throws SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + itemV1 = ItemBuilder.createItem(context, collection) .withTitle("First version") .build(); firstHandle = itemV1.getHandle(); itemV2 = VersionBuilder.createVersion(context, itemV1, "Second version").build().getItem(); itemV3 = VersionBuilder.createVersion(context, itemV1, "Third version").build().getItem(); + + context.restoreAuthSystemState(); } @Test public void testCanonicalVersionedHandleProvider() throws Exception { - registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); createVersions(); // Confirm the original item only has a version handle @@ -106,8 +106,7 @@ public void testCanonicalVersionedHandleProvider() throws Exception { @Test public void testCollectionHandleMetadata() { - registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); - + context.turnOffAuthorisationSystem(); Community testCommunity = CommunityBuilder.createCommunity(context) .withName("Test community") .build(); @@ -115,6 +114,7 @@ public void testCollectionHandleMetadata() { Collection testCollection = CollectionBuilder.createCollection(context, testCommunity) .withName("Test Collection") .build(); + context.restoreAuthSystemState(); List metadata = ContentServiceFactory.getInstance().getDSpaceObjectService(testCollection) .getMetadata(testCollection, "dc", "identifier", "uri", @@ -126,11 +126,11 @@ public void testCollectionHandleMetadata() { @Test public void testCommunityHandleMetadata() { - registerProvider(VersionedHandleIdentifierProviderWithCanonicalHandles.class); - + context.turnOffAuthorisationSystem(); Community testCommunity = CommunityBuilder.createCommunity(context) .withName("Test community") .build(); + context.restoreAuthSystemState(); List metadata = ContentServiceFactory.getInstance().getDSpaceObjectService(testCommunity) .getMetadata(testCommunity, "dc", "identifier", "uri", From b8923c986fac7cb25066c5685067935b37e8a740 Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Tue, 24 Jun 2025 07:01:56 +0200 Subject: [PATCH 157/701] fix imports for checkstyle --- .../org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java index 61e86310c340..279b3b378fc7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResourcePolicyRestRepositoryIT.java @@ -22,10 +22,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.text.SimpleDateFormat; import java.io.InputStream; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; From 230bf80b5fcde55a2abdf916c282c3439e49654b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 20:04:48 +0000 Subject: [PATCH 158/701] Bump log4j.version from 2.24.3 to 2.25.0 Bumps `log4j.version` from 2.24.3 to 2.25.0. Updates `org.apache.logging.log4j:log4j-api` from 2.24.3 to 2.25.0 Updates `org.apache.logging.log4j:log4j-core` from 2.24.3 to 2.25.0 Updates `org.apache.logging.log4j:log4j-1.2-api` from 2.24.3 to 2.25.0 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-version: 2.25.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.logging.log4j:log4j-1.2-api dependency-version: 2.25.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e819160df561..b6450fe077b0 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 1.1.1 9.4.57.v20241219 - 2.24.3 + 2.25.0 2.0.34 1.19.0 1.7.36 From b594ebbf9e6ebb838dce7a2cc5b53b8c769e98d6 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Tue, 10 Jun 2025 14:32:51 +0300 Subject: [PATCH 159/701] dspace-api: improve date parsing for Solr sort Re-use DSpace date parsing from o.d.util.MultiFormatDateParser for more robust date support when creating of Solr browse/sort indexes. --- .../java/org/dspace/sort/OrderFormatDate.java | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/sort/OrderFormatDate.java b/dspace-api/src/main/java/org/dspace/sort/OrderFormatDate.java index f56a97776f64..a8a6e5b98a00 100644 --- a/dspace-api/src/main/java/org/dspace/sort/OrderFormatDate.java +++ b/dspace-api/src/main/java/org/dspace/sort/OrderFormatDate.java @@ -7,31 +7,28 @@ */ package org.dspace.sort; +import java.util.Date; + +import org.dspace.util.MultiFormatDateParser; + /** - * Standard date ordering delegate implementation. The only "special" need is - * to treat dates with less than 4-digit year. + * Standard date ordering delegate implementation using date format + * parsing from o.d.u.MultiFormatDateParser. * * @author Andrea Bollini + * @author Alan Orth */ public class OrderFormatDate implements OrderFormatDelegate { @Override public String makeSortString(String value, String language) { - int padding = 0; - int endYearIdx = value.indexOf('-'); - - if (endYearIdx >= 0 && endYearIdx < 4) { - padding = 4 - endYearIdx; - } else if (value.length() < 4) { - padding = 4 - value.length(); - } + Date result = MultiFormatDateParser.parse(value); - if (padding > 0) { - // padding the value from left with 0 so that 87 -> 0087, 687-11-24 - // -> 0687-11-24 - return String.format("%1$0" + padding + "d", 0) - + value; + // If parsing was successful we return the value as an ISO instant, + // otherwise we return null so Solr does not index this date value. + if (result != null) { + return result.toInstant().toString(); } else { - return value; + return null; } } } From 8839eefbe7c4d67179b9fb7ea4912e4a9abcffb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 20:09:59 +0000 Subject: [PATCH 160/701] Bump org.postgresql:postgresql from 42.7.6 to 42.7.7 Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.6 to 42.7.7. - [Release notes](https://github.com/pgjdbc/pgjdbc/releases) - [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md) - [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.6...REL42.7.7) --- updated-dependencies: - dependency-name: org.postgresql:postgresql dependency-version: 42.7.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e819160df561..b13dd09b3f76 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ 5.7.14 5.6.15.Final 6.2.5.Final - 42.7.6 + 42.7.7 8.11.4 3.10.8 From 312416a716ee622efae75586112bac9a956fae08 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 20:24:13 +0000 Subject: [PATCH 161/701] Bump jersey.version from 2.46 to 2.47 Bumps `jersey.version` from 2.46 to 2.47. Updates `org.glassfish.jersey.core:jersey-client` from 2.46 to 2.47 Updates `org.glassfish.jersey.inject:jersey-hk2` from 2.46 to 2.47 Updates `org.glassfish.jersey.core:jersey-server` from 2.46 to 2.47 Updates `org.glassfish.jersey.containers:jersey-container-servlet` from 2.46 to 2.47 Updates `org.glassfish.jersey.media:jersey-media-json-jackson` from 2.46 to 2.47 Updates `org.glassfish.jersey.media:jersey-media-jaxb` from 2.46 to 2.47 Updates `org.glassfish.jersey.ext:jersey-spring5` from 2.46 to 2.47 --- updated-dependencies: - dependency-name: org.glassfish.jersey.core:jersey-client dependency-version: '2.47' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.glassfish.jersey.inject:jersey-hk2 dependency-version: '2.47' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.glassfish.jersey.core:jersey-server dependency-version: '2.47' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.glassfish.jersey.containers:jersey-container-servlet dependency-version: '2.47' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.glassfish.jersey.media:jersey-media-json-jackson dependency-version: '2.47' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.glassfish.jersey.media:jersey-media-jaxb dependency-version: '2.47' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.glassfish.jersey.ext:jersey-spring5 dependency-version: '2.47' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e819160df561..0baab9fdf1f5 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ https://jena.apache.org/documentation/migrate_jena2_jena3.html --> 2.13.0 - 2.46 + 2.47 UTF-8 From e3b917948218754ef3a02d370c4efe0dc708b849 Mon Sep 17 00:00:00 2001 From: "max.nuding" Date: Wed, 25 Jun 2025 10:36:10 +0200 Subject: [PATCH 162/701] remove unnecessary code --- .../src/main/java/org/dspace/core/Utils.java | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index b13eb3ba360c..a1294c3317ce 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -24,9 +24,6 @@ import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -510,47 +507,4 @@ public static String interpolateConfigsInString(String string) { return StringSubstitutor.replace(string, config.getProperties()); } - /** - * Get the maximum timestamp that can be stored in a PostgreSQL database with hibernate, - * for our "distant future" access expiry date. - * @return the maximum timestamp that can be stored with Postgres + Hibernate - */ - public static Instant getMaxTimestamp() { - return LocalDateTime.of(294276, 12, 31, 23, 59, 59) - .toInstant(ZoneOffset.UTC); - } - - /** - * Get the minimum timestamp that can be stored in a PostgreSQL database, for date validation or any other - * purpose to ensure we don't try to store a date before the epoch. - * @return the minimum timestamp that can be stored with Postgres + Hibernate - */ - public static Instant getMinTimestamp() { - return LocalDateTime.of(-4713, 11, 12, 0, 0, 0) - .toInstant(ZoneOffset.UTC); - } - - /** - * Checks whether a given string can be converted to a valid {@code int} value. - *

- * This method returns {@code false} if the input string is {@code null}, empty, - * or contains only whitespace. Otherwise, it attempts to parse the string as an - * integer using {@link Integer#parseInt(String)}. - * - * @param str the string to check for integer convertibility - * @return {@code true} if the string is non-blank and can be parsed as an integer; - * {@code false} otherwise - */ - public static boolean isConvertibleToInt(String str) { - if (StringUtils.isBlank(str)) { - return false; - } - try { - Integer.parseInt(str); - return true; - } catch (NumberFormatException e) { - return false; - } - } - } From 57a1de5ecdb1a1bec180bf73a05839b8e8b1340a Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 25 Jun 2025 16:41:48 -0500 Subject: [PATCH 163/701] Update deploy demo.dspace.org branch to 9.x to ensure older branches never trigger a redeploy. --- .github/workflows/reusable-docker-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index aec03603cef0..528f5779ca96 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -73,7 +73,7 @@ env: REDEPLOY_DEMO_URL: ${{ secrets.REDEPLOY_DEMO_URL }} # Current DSpace maintenance branch (and architecture) which is deployed to demo.dspace.org / sandbox.dspace.org # (NOTE: No deployment branch specified for sandbox.dspace.org as it uses the default_branch) - DEPLOY_DEMO_BRANCH: 'dspace-8_x' + DEPLOY_DEMO_BRANCH: 'dspace-9_x' DEPLOY_SANDBOX_BRANCH: 'main' DEPLOY_ARCH: 'linux/amd64' # Registry used during building of Docker images. (All images are later copied to docker.io registry) From d0b5911cf644cb27b33887e7e69d71696cf18ce6 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 11 Jun 2025 15:43:10 -0400 Subject: [PATCH 164/701] Make POI record buffer size adjustable. --- .../mediafilter/TikaTextExtractionFilter.java | 18 ++++++++++++++---- dspace/config/dspace.cfg | 7 +++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java index b7a6063165b7..5728f4f42f48 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/TikaTextExtractionFilter.java @@ -18,6 +18,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.poi.util.IOUtils; import org.apache.tika.Tika; import org.apache.tika.exception.TikaException; import org.apache.tika.metadata.Metadata; @@ -37,6 +38,8 @@ public class TikaTextExtractionFilter extends MediaFilter { private final static Logger log = LogManager.getLogger(); + private static final int DEFAULT_MAX_CHARS = 100_000; + private static final int DEFAULT_MAX_ARRAY = 100_000_000; @Override public String getFilteredName(String oldFilename) { @@ -70,9 +73,12 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo } // Not using temporary file. We'll use Tika's default in-memory parsing. - // Get maximum characters to extract. Default is 100,000 chars, which is also Tika's default setting. String extractedText; - int maxChars = configurationService.getIntProperty("textextractor.max-chars", 100000); + // Get maximum characters to extract. Default is 100,000 chars, which is also Tika's default setting. + int maxChars = configurationService.getIntProperty("textextractor.max-chars", DEFAULT_MAX_CHARS); + // Get maximum size of structure that Tika will try to buffer. + int maxArray = configurationService.getIntProperty("textextractor.max-array", DEFAULT_MAX_ARRAY); + IOUtils.setByteArrayMaxOverride(maxArray); try { // Use Tika to extract text from input. Tika will automatically detect the file type. Tika tika = new Tika(); @@ -80,13 +86,13 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo extractedText = tika.parseToString(source); } catch (IOException e) { System.err.format("Unable to extract text from bitstream in Item %s%n", currentItem.getID().toString()); - e.printStackTrace(); + e.printStackTrace(System.err); log.error("Unable to extract text from bitstream in Item {}", currentItem.getID().toString(), e); throw e; } catch (OutOfMemoryError oe) { System.err.format("OutOfMemoryError occurred when extracting text from bitstream in Item %s. " + "You may wish to enable 'textextractor.use-temp-file'.%n", currentItem.getID().toString()); - oe.printStackTrace(); + oe.printStackTrace(System.err); log.error("OutOfMemoryError occurred when extracting text from bitstream in Item {}. " + "You may wish to enable 'textextractor.use-temp-file'.", currentItem.getID().toString(), oe); throw oe; @@ -167,6 +173,10 @@ public void ignorableWhitespace(char[] ch, int start, int length) throws SAXExce } }); + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + int maxArray = configurationService.getIntProperty("textextractor.max-array", DEFAULT_MAX_ARRAY); + IOUtils.setByteArrayMaxOverride(maxArray); + AutoDetectParser parser = new AutoDetectParser(); Metadata metadata = new Metadata(); // parse our source InputStream using the above custom handler diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 4621ca51dc13..c0600dbcebde 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -523,6 +523,13 @@ filter.org.dspace.app.mediafilter.PDFBoxThumbnail.inputFormats = Adobe PDF # text ("filter-media -f" ) and then reindex your site ("index-discovery -b"). #textextractor.use-temp-file = false +# Maximum size of a record buffer for text extraction. Set this if you are +# seeing RecordFormatException calling out excessive array length from +# 'dspace filter-media'. It is likely that you will need to increase the +# size of the Java heap if you greatly increase this value -- see JAVA_OPTS +# in 'bin/dspace' or 'bin/dspace/bat'. +#textextractor.max-array = 1000000 + # Custom settigns for ImageMagick Thumbnail Filters # ImageMagick and GhostScript must be installed on the server, set the path to ImageMagick and GhostScript executable # http://www.imagemagick.org/ From 172783691b29ecd21bb75fb5ddbda80ab0b9f77d Mon Sep 17 00:00:00 2001 From: abhinav Date: Wed, 25 Jun 2025 16:01:22 +0200 Subject: [PATCH 165/701] fix metadata getting cleared on patch request with invalid field (cherry picked from commit e559af1841ba52890279fdc5efdcc441e7f9eb79) --- .../DSpaceObjectMetadataReplaceOperation.java | 3 +-- .../org/dspace/app/rest/PatchMetadataIT.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java index 1cf15684587b..a3de8d3c9ea6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java @@ -80,8 +80,7 @@ private void replace(Context context, DSpaceObject dso, DSpaceObjectService dsoS MetadataValueRest metadataValue, String index, String propertyOfMd, String valueMdProperty) { // replace entire set of metadata if (metadataField == null) { - this.replaceAllMetadata(context, dso, dsoService); - return; + throw new UnprocessableEntityException("Metadata field does not exist"); } // replace all metadata for existing key diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java index 840792f31b92..95ebb480bcf1 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java @@ -1426,6 +1426,27 @@ public void moveMetadataAuthorFourToOneMultiOpTest() throws Exception { moveMetadataAuthorTest(moves, expectedOrder); } + @Test + public void replaceInvalidMetadataShouldFailTest() throws Exception { + initSimplePublicationItem(); + assertEquals(12, publicationItem.getMetadata().size()); + + String patchBody = getPatchContent(List.of( + new ReplaceOperation("/metadata/dc.contributor.invalid/0", "some value") + )); + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(patch("/api/core/items/" + publicationItem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnprocessableEntity()); + + publicationItem = context.reloadEntity(publicationItem); + + assertEquals(12, publicationItem.getMetadata().size()); + assertEquals(0, + itemService.getMetadata(publicationItem, "dc", "contributor", "invalid", Item.ANY, false).size()); + } + /** * This method moves an author (dc.contributor.author) within a workspace publication's "traditionalpageone" * section from position "from" to "path" using a PATCH request and verifies the order of the authors within the From 934f73f412c6deb3e3c17e9c97d96493f4aa4029 Mon Sep 17 00:00:00 2001 From: abhinav Date: Wed, 25 Jun 2025 16:19:56 +0200 Subject: [PATCH 166/701] Update PatchMetadataIT (cherry picked from commit 27d59085dbcdc702bf779a309b4ed11148ebb5dc) --- .../src/test/java/org/dspace/app/rest/PatchMetadataIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java index 95ebb480bcf1..368128fd6e4c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java @@ -1429,7 +1429,7 @@ public void moveMetadataAuthorFourToOneMultiOpTest() throws Exception { @Test public void replaceInvalidMetadataShouldFailTest() throws Exception { initSimplePublicationItem(); - assertEquals(12, publicationItem.getMetadata().size()); + assertEquals(11, publicationItem.getMetadata().size()); String patchBody = getPatchContent(List.of( new ReplaceOperation("/metadata/dc.contributor.invalid/0", "some value") @@ -1442,7 +1442,7 @@ public void replaceInvalidMetadataShouldFailTest() throws Exception { publicationItem = context.reloadEntity(publicationItem); - assertEquals(12, publicationItem.getMetadata().size()); + assertEquals(11, publicationItem.getMetadata().size()); assertEquals(0, itemService.getMetadata(publicationItem, "dc", "contributor", "invalid", Item.ANY, false).size()); } From 130442746a96819fc8ca2eac9aad7a1f541bc885 Mon Sep 17 00:00:00 2001 From: abhinav Date: Thu, 26 Jun 2025 09:25:40 +0200 Subject: [PATCH 167/701] move the exception to patchUtils (cherry picked from commit a2dc6fbdf89e15db0d51d261dd1708e00a526d27) --- .../operation/DSpaceObjectMetadataPatchUtils.java | 15 +++++++++++++-- .../DSpaceObjectMetadataReplaceOperation.java | 3 ++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataPatchUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataPatchUtils.java index 954cc844f237..5bbd5c013c9e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataPatchUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataPatchUtils.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.JsonValueEvaluator; import org.dspace.app.rest.model.patch.Operation; @@ -135,10 +136,20 @@ protected String extractNewValueOfMd(Operation operation) { * @param context Context the retrieve metadataField from service with string * @param operation Operation of the patch * @return The metadataField corresponding to the md element string of the operation + * Null if no metadata field is passed in the operation + * @throws UnprocessableEntityException if an invalid metadata field is passed in the operation */ - protected MetadataField getMetadataField(Context context, Operation operation) throws SQLException { + protected MetadataField getMetadataField(Context context, Operation operation) + throws SQLException, UnprocessableEntityException { String mdElement = this.extractMdFieldStringFromOperation(operation); - return metadataFieldService.findByString(context, mdElement, '.'); + if (StringUtils.isBlank(mdElement)) { + return null; + } + MetadataField metadataField = metadataFieldService.findByString(context, mdElement, '.'); + if (metadataField == null) { + throw new UnprocessableEntityException("Metadata field does not exist"); + } + return metadataField; } /** diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java index a3de8d3c9ea6..1cf15684587b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/DSpaceObjectMetadataReplaceOperation.java @@ -80,7 +80,8 @@ private void replace(Context context, DSpaceObject dso, DSpaceObjectService dsoS MetadataValueRest metadataValue, String index, String propertyOfMd, String valueMdProperty) { // replace entire set of metadata if (metadataField == null) { - throw new UnprocessableEntityException("Metadata field does not exist"); + this.replaceAllMetadata(context, dso, dsoService); + return; } // replace all metadata for existing key From aac45284d239a54a036eefe455c6f75bb0cd1655 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 27 Jun 2025 13:05:51 -0500 Subject: [PATCH 168/701] Correct metadata value count for dspace-7_x --- .../src/test/java/org/dspace/app/rest/PatchMetadataIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java index 368128fd6e4c..95ebb480bcf1 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PatchMetadataIT.java @@ -1429,7 +1429,7 @@ public void moveMetadataAuthorFourToOneMultiOpTest() throws Exception { @Test public void replaceInvalidMetadataShouldFailTest() throws Exception { initSimplePublicationItem(); - assertEquals(11, publicationItem.getMetadata().size()); + assertEquals(12, publicationItem.getMetadata().size()); String patchBody = getPatchContent(List.of( new ReplaceOperation("/metadata/dc.contributor.invalid/0", "some value") @@ -1442,7 +1442,7 @@ public void replaceInvalidMetadataShouldFailTest() throws Exception { publicationItem = context.reloadEntity(publicationItem); - assertEquals(11, publicationItem.getMetadata().size()); + assertEquals(12, publicationItem.getMetadata().size()); assertEquals(0, itemService.getMetadata(publicationItem, "dc", "contributor", "invalid", Item.ANY, false).size()); } From 66a75f522faeba45dc3081460d33a43aa9a864ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:37:25 +0000 Subject: [PATCH 169/701] Bump the fasterxml group with 3 updates Bumps the fasterxml group with 3 updates: [com.fasterxml.jackson.core:jackson-annotations](https://github.com/FasterXML/jackson), [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) and [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson). Updates `com.fasterxml.jackson.core:jackson-annotations` from 2.19.0 to 2.19.1 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.core:jackson-core` from 2.19.0 to 2.19.1 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.19.0...jackson-core-2.19.1) Updates `com.fasterxml.jackson.core:jackson-core` from 2.19.0 to 2.19.1 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.19.0...jackson-core-2.19.1) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.19.0 to 2.19.1 - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-annotations dependency-version: 2.19.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.19.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.19.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.19.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 6524cf95c093..928d60d1a77d 100644 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 3.10.8 2.31.0 - 2.19.0 - 2.19.0 + 2.19.1 + 2.19.1 1.3.2 2.3.1 2.3.9 From 76c50ac43e929a9ce702b674bb6fdf4335e471ce Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 30 Jun 2025 16:26:48 -0500 Subject: [PATCH 170/701] Update POM to use central-publishing-maven-plugin and Sonatype's Central Portal --- pom.xml | 77 +++++++++++++++++++++++++-------------------------------- 1 file changed, 33 insertions(+), 44 deletions(-) diff --git a/pom.xml b/pom.xml index 928d60d1a77d..85fb3ce6ccd7 100644 --- a/pom.xml +++ b/pom.xml @@ -355,9 +355,9 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.7.0 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 @@ -938,7 +938,7 @@ + - org.sonatype.plugins - nexus-staging-maven-plugin + org.sonatype.central + central-publishing-maven-plugin true - - - ossrh - https://oss.sonatype.org/ - - true - - false - - 10 - - + org.apache.maven.plugins maven-source-plugin @@ -987,7 +985,7 @@ - + org.apache.maven.plugins maven-javadoc-plugin @@ -1000,8 +998,8 @@ - + org.apache.maven.plugins maven-gpg-plugin @@ -1940,35 +1938,23 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git - git@github.com:DSpace/DSpace.git + https://github.com/DSpace/DSpace dspace-7_x - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - maven-central + central https://repo.maven.apache.org/maven2 + + false + - + - maven-snapshots - https://oss.sonatype.org/content/repositories/snapshots - default + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ false @@ -1980,6 +1966,9 @@ handle.net https://handle.net/maven + + false + From 8f9a7f1f92ca320476b110f595d70bcc208bbe44 Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Wed, 2 Jul 2025 10:24:18 +0200 Subject: [PATCH 171/701] Point directly to HTTPS address for ArXiv (cherry picked from commit cf0d6635f2683b1c8a9c116d33a3e82779e70f77) --- .../resources/spring/spring-dspace-addon-import-services.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index 6b0ef3e9b9e3..c758966f449d 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -45,7 +45,7 @@ - + From f7dcbf1b44fb2d973000cedadb4d1f5a48bd70a1 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 2 Jul 2025 10:39:50 +0200 Subject: [PATCH 172/701] Safe and consistent XML entity handling in parsers --- .../dspace/administer/RegistryImporter.java | 7 +- .../org/dspace/administer/RegistryLoader.java | 9 +- .../org/dspace/administer/StructBuilder.java | 6 +- .../app/itemimport/ItemImportServiceImpl.java | 8 +- .../dspace/app/itemupdate/ItemArchive.java | 18 +-- .../dspace/app/launcher/ScriptLauncher.java | 3 +- .../app/sfx/SFXFileReaderServiceImpl.java | 5 +- .../org/dspace/app/util/DCInputsReader.java | 6 +- .../dspace/app/util/InitializeEntities.java | 6 +- .../app/util/SubmissionConfigReader.java | 9 +- .../java/org/dspace/app/util/XMLUtils.java | 123 ++++++++++++++++++ .../crosswalk/METSDisseminationCrosswalk.java | 3 +- .../crosswalk/MODSDisseminationCrosswalk.java | 3 +- .../content/crosswalk/QDCCrosswalk.java | 3 +- .../content/crosswalk/RoleCrosswalk.java | 3 +- .../crosswalk/XSLTIngestionCrosswalk.java | 3 +- .../dspace/content/packager/METSManifest.java | 8 +- .../dspace/content/packager/RoleIngester.java | 4 +- .../ctask/general/MetadataWebService.java | 11 +- .../provider/orcid/xml/Converter.java | 4 +- .../identifier/doi/DataCiteConnector.java | 3 +- .../ArXivImportMetadataSourceServiceImpl.java | 5 +- .../CiniiImportMetadataSourceServiceImpl.java | 15 +-- .../crossref/CrossRefAbstractProcessor.java | 5 +- .../EpoImportMetadataSourceServiceImpl.java | 13 +- ...PubmedImportMetadataSourceServiceImpl.java | 20 ++- ...PubmedEuropeMetadataSourceServiceImpl.java | 11 +- ...ScopusImportMetadataSourceServiceImpl.java | 9 +- .../WOSImportMetadataSourceServiceImpl.java | 11 +- .../CCLicenseConnectorServiceImpl.java | 3 +- .../dspace/orcid/client/OrcidClientImpl.java | 4 +- .../vocabulary/ControlledVocabulary.java | 4 +- 32 files changed, 224 insertions(+), 121 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java b/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java index 27a653421312..c74e56bce890 100644 --- a/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java +++ b/dspace-api/src/main/java/org/dspace/administer/RegistryImporter.java @@ -10,7 +10,6 @@ import java.io.File; import java.io.IOException; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -18,6 +17,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.dspace.app.util.XMLUtils; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -49,8 +49,9 @@ private RegistryImporter() { } */ public static Document loadXML(String filename) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); + // This XML builder will *not* disable external entities as XML + // registries are considered trusted content + DocumentBuilder builder = XMLUtils.getTrustedDocumentBuilder(); Document document = builder.parse(new File(filename)); diff --git a/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java b/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java index d503bfc00b7f..8bb72e18521e 100644 --- a/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java +++ b/dspace-api/src/main/java/org/dspace/administer/RegistryLoader.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -29,6 +28,7 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.BitstreamFormat; import org.dspace.content.factory.ContentServiceFactory; @@ -266,8 +266,9 @@ private static void loadFormat(Context context, Node node) */ private static Document loadXML(String filename) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); + // This XML builder will *not* disable external entities as XML + // registries are considered trusted content + DocumentBuilder builder = XMLUtils.getTrustedDocumentBuilder(); return builder.parse(new File(filename)); } @@ -351,4 +352,4 @@ private static String[] getRepeatedElementData(Node parentElement, return data; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java index 8bbcfe0ff753..f2577a37b176 100644 --- a/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java +++ b/dspace-api/src/main/java/org/dspace/administer/StructBuilder.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -43,6 +42,7 @@ import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -613,8 +613,8 @@ private static String validateCollections(NodeList collections, int level) */ private static org.w3c.dom.Document loadXML(InputStream input) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); + // This builder factory does not disable external DTD, entities, etc. + DocumentBuilder builder = XMLUtils.getTrustedDocumentBuilder(); org.w3c.dom.Document document = builder.parse(input); diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java index 1e219ee6314c..3af383b04caf 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java @@ -48,7 +48,6 @@ import java.util.zip.ZipFile; import javax.mail.MessagingException; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -67,6 +66,7 @@ import org.dspace.app.itemimport.service.ItemImportService; import org.dspace.app.util.LocalSchemaFilenameFilter; import org.dspace.app.util.RelationshipUtils; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; @@ -179,6 +179,8 @@ public class ItemImportServiceImpl implements ItemImportService, InitializingBea @Autowired(required = true) protected MetadataValueService metadataValueService; + protected DocumentBuilder builder; + protected String tempWorkDir; protected boolean isTest = false; @@ -1888,9 +1890,7 @@ protected String getStringValue(Node node) { */ protected Document loadXML(String filename) throws IOException, ParserConfigurationException, SAXException { - DocumentBuilder builder = DocumentBuilderFactory.newInstance() - .newDocumentBuilder(); - + DocumentBuilder builder = XMLUtils.getDocumentBuilder(); return builder.parse(new File(filename)); } diff --git a/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemArchive.java b/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemArchive.java index 26de45caf77e..7dda65a0a75b 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemArchive.java +++ b/dspace-api/src/main/java/org/dspace/app/itemupdate/ItemArchive.java @@ -23,8 +23,6 @@ import java.util.Iterator; import java.util.List; import java.util.UUID; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; @@ -33,6 +31,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.app.util.LocalSchemaFilenameFilter; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -52,7 +51,6 @@ public class ItemArchive { public static final String DUBLIN_CORE_XML = "dublin_core.xml"; - protected static DocumentBuilder builder = null; protected Transformer transformer = null; protected List dtomList = null; @@ -95,14 +93,14 @@ public static ItemArchive create(Context context, File dir, String itemField) InputStream is = null; try { is = new FileInputStream(new File(dir, DUBLIN_CORE_XML)); - itarch.dtomList = MetadataUtilities.loadDublinCore(getDocumentBuilder(), is); + itarch.dtomList = MetadataUtilities.loadDublinCore(XMLUtils.getDocumentBuilder(), is); //The code to search for local schema files was copied from org.dspace.app.itemimport // .ItemImportServiceImpl.java File file[] = dir.listFiles(new LocalSchemaFilenameFilter()); for (int i = 0; i < file.length; i++) { is = new FileInputStream(file[i]); - itarch.dtomList.addAll(MetadataUtilities.loadDublinCore(getDocumentBuilder(), is)); + itarch.dtomList.addAll(MetadataUtilities.loadDublinCore(XMLUtils.getDocumentBuilder(), is)); } } finally { if (is != null) { @@ -126,14 +124,6 @@ public static ItemArchive create(Context context, File dir, String itemField) return itarch; } - protected static DocumentBuilder getDocumentBuilder() - throws ParserConfigurationException { - if (builder == null) { - builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - } - return builder; - } - /** * Getter for Transformer * @@ -318,7 +308,7 @@ public void writeUndo(File undoDir) try { out = new FileOutputStream(new File(dir, "dublin_core.xml")); - Document doc = MetadataUtilities.writeDublinCore(getDocumentBuilder(), undoDtomList); + Document doc = MetadataUtilities.writeDublinCore(XMLUtils.getDocumentBuilder(), undoDtomList); MetadataUtilities.writeDocument(doc, getTransformer(), out); // if undo has delete bitstream diff --git a/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java b/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java index 89a416bfa883..ab8807c2cae3 100644 --- a/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java +++ b/dspace-api/src/main/java/org/dspace/app/launcher/ScriptLauncher.java @@ -19,6 +19,7 @@ import org.apache.commons.cli.ParseException; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.core.Context; import org.dspace.scripts.DSpaceRunnable; import org.dspace.scripts.DSpaceRunnable.StepResult; @@ -314,7 +315,7 @@ public static Document getConfig(DSpaceKernelImpl kernelImpl) { String config = kernelImpl.getConfigurationService().getProperty("dspace.dir") + System.getProperty("file.separator") + "config" + System.getProperty("file.separator") + "launcher.xml"; - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document doc = null; try { doc = saxBuilder.build(config); diff --git a/dspace-api/src/main/java/org/dspace/app/sfx/SFXFileReaderServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/sfx/SFXFileReaderServiceImpl.java index 184f00a53e59..d3b447374a2c 100644 --- a/dspace-api/src/main/java/org/dspace/app/sfx/SFXFileReaderServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/sfx/SFXFileReaderServiceImpl.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.sfx.service.SFXFileReaderService; +import org.dspace.app.util.XMLUtils; import org.dspace.content.DCPersonName; import org.dspace.content.Item; import org.dspace.content.MetadataValue; @@ -79,9 +80,9 @@ public Document parseFile(String fileName) { log.info("Parsing XML file... " + fileName); DocumentBuilder docBuilder; Document doc = null; - DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); - docBuilderFactory.setIgnoringElementContentWhitespace(true); try { + DocumentBuilderFactory docBuilderFactory = XMLUtils.getDocumentBuilderFactory(); + docBuilderFactory.setIgnoringElementContentWhitespace(true); docBuilder = docBuilderFactory.newDocumentBuilder(); } catch (ParserConfigurationException e) { log.error("Wrong parser configuration: " + e.getMessage()); diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index c77c3bf10e08..d2f3ac0dc5d9 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java @@ -121,7 +121,11 @@ private void buildInputs(String fileName) String uri = "file:" + new File(fileName).getAbsolutePath(); try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + // This document builder factory will *not* disable external + // entities as they can be useful in managing large forms, but + // it is up to site administrators to validate the XML they are + // storing + DocumentBuilderFactory factory = XMLUtils.getTrustedDocumentBuilderFactory(); factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); diff --git a/dspace-api/src/main/java/org/dspace/app/util/InitializeEntities.java b/dspace-api/src/main/java/org/dspace/app/util/InitializeEntities.java index 8d3964a3e3c7..4e64c18fcedd 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/InitializeEntities.java +++ b/dspace-api/src/main/java/org/dspace/app/util/InitializeEntities.java @@ -11,7 +11,6 @@ import java.io.IOException; import java.sql.SQLException; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.cli.CommandLine; @@ -139,8 +138,9 @@ private void run(String fileLocation) throws SQLException, AuthorizeException { private void parseXMLToRelations(Context context, String fileLocation) throws AuthorizeException { try { File fXmlFile = new File(fileLocation); - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + // This XML builder will allow external entities, so the relationship types XML should + // be considered trusted by administrators + DocumentBuilder dBuilder = XMLUtils.getTrustedDocumentBuilder(); Document doc = dBuilder.parse(fXmlFile); doc.getDocumentElement().normalize(); diff --git a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java index 70c5092602a3..1914ded86b66 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java @@ -160,8 +160,11 @@ private void buildInputs(String fileName) throws SubmissionConfigReaderException String uri = "file:" + new File(fileName).getAbsolutePath(); try { - DocumentBuilderFactory factory = DocumentBuilderFactory - .newInstance(); + // This document builder factory will *not* disable external + // entities as they can be useful in managing large forms, but + // it is up to site administrators to validate the XML they are + // storing + DocumentBuilderFactory factory = XMLUtils.getTrustedDocumentBuilderFactory(); factory.setValidating(false); factory.setIgnoringComments(true); factory.setIgnoringElementContentWhitespace(true); @@ -681,4 +684,4 @@ public List getCollectionsBySubmissionConfig(Context context, String } return results; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java b/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java index c39d0d26fd5e..389d53fe6da4 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java +++ b/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java @@ -9,8 +9,13 @@ import java.util.ArrayList; import java.util.List; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.XMLInputFactory; import org.apache.commons.lang3.StringUtils; +import org.jdom2.input.SAXBuilder; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -161,4 +166,122 @@ public static List getElementValueArrayList(Element rootElement, } return result; } + + /** + * Initialize and return a javax DocumentBuilderFactory with NO security + * applied. This is intended only for internal, administrative/configuration + * use where external entities and other dangerous features are actually + * purposefully included. + * The method here is tiny, but may be expanded with other features like + * whitespace handling, and calling this method name helps to document + * the fact that the caller knows it is trusting the XML source / factory. + * + * @return document builder factory to generate new builders + * @throws ParserConfigurationException + */ + public static DocumentBuilderFactory getTrustedDocumentBuilderFactory() + throws ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + return factory; + } + + /** + * Initialize and return the javax DocumentBuilderFactory with some basic security + * applied to avoid XXE attacks and other unwanted content inclusion + * @return document builder factory to generate new builders + * @throws ParserConfigurationException + */ + public static DocumentBuilderFactory getDocumentBuilderFactory() + throws ParserConfigurationException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + // No DOCTYPE / DTDs + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + // No external general entities + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + // No external parameter entities + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + // No external DTDs + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + // Even if entities somehow get defined, they will not be expanded + factory.setExpandEntityReferences(false); + // Disable "XInclude" markup processing + factory.setXIncludeAware(false); + + return factory; + } + + /** + * Initialize and return a javax DocumentBuilder with NO security + * applied. This is intended only for internal, administrative/configuration + * use where external entities and other dangerous features are actually + * purposefully included. + * The method here is tiny, but may be expanded with other features like + * whitespace handling, and calling this method name helps to document + * the fact that the caller knows it is trusting the XML source / builder + * + * @return document builder with no security features set + * @throws ParserConfigurationException + */ + public static DocumentBuilder getTrustedDocumentBuilder() + throws ParserConfigurationException { + return getTrustedDocumentBuilderFactory().newDocumentBuilder(); + } + + /** + * Initialize and return the javax DocumentBuilder with some basic security applied + * to avoid XXE attacks and other unwanted content inclusion + * @return document builder for use in XML parsing + * @throws ParserConfigurationException + */ + public static DocumentBuilder getDocumentBuilder() + throws ParserConfigurationException { + return getDocumentBuilderFactory().newDocumentBuilder(); + } + + /** + * Initialize and return the SAX document builder with some basic security applied + * to avoid XXE attacks and other unwanted content inclusion + * @return SAX document builder for use in XML parsing + */ + public static SAXBuilder getSAXBuilder() { + return getSAXBuilder(false); + } + + /** + * Initialize and return the SAX document builder with some basic security applied + * to avoid XXE attacks and other unwanted content inclusion + * @param validate whether to use JDOM XSD validation + * @return SAX document builder for use in XML parsing + */ + public static SAXBuilder getSAXBuilder(boolean validate) { + SAXBuilder saxBuilder = new SAXBuilder(); + if (validate) { + saxBuilder.setValidation(true); + } + // No DOCTYPE / DTDs + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + // No external general entities + saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false); + // No external parameter entities + saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + // No external DTDs + saxBuilder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + // Don't expand entities + saxBuilder.setExpandEntities(false); + + return saxBuilder; + } + + /** + * Initialize and return the Java XML Input Factory with some basic security applied + * to avoid XXE attacks and other unwanted content inclusion + * @return XML input factory for use in XML parsing + */ + public static XMLInputFactory getXMLInputFactory() { + XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + + return xmlInputFactory; + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java index b8a4a8aef390..5ceacc933e4c 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java @@ -14,6 +14,7 @@ import java.util.List; import org.apache.commons.lang3.ArrayUtils; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.packager.PackageDisseminator; @@ -129,7 +130,7 @@ public Element disseminateElement(Context context, DSpaceObject dso) try { //Return just the root Element of the METS file - SAXBuilder builder = new SAXBuilder(); + SAXBuilder builder = XMLUtils.getSAXBuilder(); Document metsDocument = builder.build(tempFile); return metsDocument.getRootElement(); } catch (JDOMException je) { diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java index 1e63be5ba1b9..205b3ef5b343 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/MODSDisseminationCrosswalk.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -144,7 +145,7 @@ public static String[] getPluginNames() { MODS_NS.getURI() + " " + MODS_XSD; private static final XMLOutputter outputUgly = new XMLOutputter(); - private static final SAXBuilder builder = new SAXBuilder(); + private static final SAXBuilder builder = XMLUtils.getSAXBuilder(); private Map modsMap = null; diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java index 2fdbaaad003e..51e6357d93e1 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/QDCCrosswalk.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -125,7 +126,7 @@ public class QDCCrosswalk extends SelfNamedPlugin // XML schemaLocation fragment for this crosswalk, from config. private String schemaLocation = null; - private static final SAXBuilder builder = new SAXBuilder(); + private static final SAXBuilder builder = XMLUtils.getSAXBuilder(); protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java index 2c763036ce33..8d5bf49902cc 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/RoleCrosswalk.java @@ -13,6 +13,7 @@ import java.sql.SQLException; import java.util.List; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.packager.PackageDisseminator; @@ -208,7 +209,7 @@ public Element disseminateElement(Context context, DSpaceObject dso) try { //Try to parse our XML results (which were disseminated by the Packager) - SAXBuilder builder = new SAXBuilder(); + SAXBuilder builder = XMLUtils.getSAXBuilder(); Document xmlDocument = builder.build(tempFile); //If XML parsed successfully, return root element of doc if (xmlDocument != null && xmlDocument.hasRootElement()) { diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java index 63ef5f7336c7..b07b2b2228e4 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTIngestionCrosswalk.java @@ -18,6 +18,7 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -297,7 +298,7 @@ public static void main(String[] argv) throws Exception { "Failed to initialize transformer, probably error loading stylesheet."); } - SAXBuilder builder = new SAXBuilder(); + SAXBuilder builder = XMLUtils.getSAXBuilder(); Document inDoc = builder.build(new FileInputStream(argv[i + 1])); XMLOutputter outputter = new XMLOutputter(Format.getPrettyFormat()); List dimList; diff --git a/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java b/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java index 3399bdf0f07e..a1ed3c124374 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/METSManifest.java @@ -20,6 +20,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; @@ -265,12 +266,13 @@ protected METSManifest(SAXBuilder builder, Element mets, String configName) { public static METSManifest create(InputStream is, boolean validate, String configName) throws IOException, MetadataValidationException { - SAXBuilder builder = new SAXBuilder(validate); + SAXBuilder builder = XMLUtils.getSAXBuilder(); builder.setIgnoringElementContentWhitespace(true); // Set validation feature if (validate) { + builder.setValidation(true); builder.setFeature("http://apache.org/xml/features/validation/schema", true); // Tell the parser where local copies of schemas are, to speed up @@ -278,10 +280,6 @@ public static METSManifest create(InputStream is, boolean validate, String confi if (localSchemas.length() > 0) { builder.setProperty("http://apache.org/xml/properties/schema/external-schemaLocation", localSchemas); } - } else { - // disallow DTD parsing to ensure no XXE attacks can occur. - // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html - builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); } // Parse the METS file diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java index 2ce3f50a3cbc..d71012ff8356 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java @@ -386,7 +386,7 @@ public void ingestStream(Context context, DSpaceObject parent, Document document; try { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilderFactory dbf = XMLUtils.getDocumentBuilderFactory(); dbf.setIgnoringComments(true); dbf.setCoalescing(true); DocumentBuilder db = dbf.newDocumentBuilder(); @@ -420,7 +420,7 @@ public DSpaceObject ingest(Context context, DSpaceObject parent, Document document; try { - DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilderFactory dbf = XMLUtils.getDocumentBuilderFactory(); dbf.setIgnoringComments(true); dbf.setCoalescing(true); DocumentBuilder db = dbf.newDocumentBuilder(); diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java index fc62d7a4b23f..1b618ad31fb7 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/MetadataWebService.java @@ -37,6 +37,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -176,7 +177,7 @@ public void init(Curator curator, String taskId) throws IOException { fieldSeparator = (fldSep != null) ? fldSep : " "; urlTemplate = taskProperty("template"); templateParam = urlTemplate.substring(urlTemplate.indexOf("{") + 1, - urlTemplate.indexOf("}")); + urlTemplate.indexOf("}")); String[] parsed = parseTransform(templateParam); lookupField = parsed[0]; lookupTransform = parsed[1]; @@ -204,13 +205,9 @@ public void init(Curator curator, String taskId) throws IOException { } } // initialize response document parser - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(true); try { - // disallow DTD parsing to ensure no XXE attacks can occur - // See https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - factory.setXIncludeAware(false); + DocumentBuilderFactory factory = XMLUtils.getDocumentBuilderFactory(); + factory.setNamespaceAware(true); docBuilder = factory.newDocumentBuilder(); } catch (ParserConfigurationException pcE) { log.error("caught exception: " + pcE); diff --git a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java index 756b8654f285..af4e31e7fae2 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java @@ -31,9 +31,7 @@ public abstract class Converter { protected Object unmarshall(InputStream input, Class type) throws SAXException, URISyntaxException { try { - XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); - // disallow DTD parsing to ensure no XXE attacks can occur - xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + XMLInputFactory xmlInputFactory = XMLUtils.getXMLInputFactory(); XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(input); JAXBContext context = JAXBContext.newInstance(type); diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index 86c7b8322e81..c8e316eaf66c 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java @@ -38,6 +38,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.util.EntityUtils; import org.dspace.app.client.DSpaceHttpClientFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.crosswalk.CrosswalkException; @@ -832,7 +833,7 @@ protected String extractAlternateIdentifier(Context context, String content) } // parse the XML - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document doc = null; try { doc = saxBuilder.build(new ByteArrayInputStream(content.getBytes("UTF-8"))); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java index 4369b0d48b46..a8d01fc3fd28 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/arxiv/service/ArXivImportMetadataSourceServiceImpl.java @@ -22,6 +22,7 @@ import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -218,7 +219,7 @@ public Integer call() throws Exception { if (response.getStatus() == 200) { String responseString = response.readEntity(String.class); - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(responseString)); Element root = document.getRootElement(); @@ -399,7 +400,7 @@ private String getQuery(Query query) { private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java index 82a4b2d77968..41c80ab7fe64 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java @@ -26,6 +26,7 @@ import org.apache.http.client.utils.URIBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -301,9 +302,7 @@ protected List search(String id, String appId) private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); return root.getChildren(); @@ -356,9 +355,7 @@ private List getCiniiIds(String appId, Integer maxResult, String author, Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); int url_len = this.url.length() - 1; - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); List namespaces = Arrays.asList( @@ -420,9 +417,7 @@ private Integer countCiniiElement(String appId, Integer maxResult, String author Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); List namespaces = Arrays @@ -449,4 +444,4 @@ private MetadatumDTO createIdentifier(String id) { return metadatumDTO; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java index 1b6da9d37b16..99f1ee37a54e 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.Collection; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import com.fasterxml.jackson.core.JsonProcessingException; @@ -21,6 +20,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.importer.external.metadatamapping.contributor.JsonPathMetadataProcessor; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -64,10 +64,9 @@ private String prettifyAbstract(String abstractValue) { } String xmlString = "" + abstractValue + ""; - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Document xmlDoc; try { - DocumentBuilder builder = factory.newDocumentBuilder(); + DocumentBuilder builder = XMLUtils.getDocumentBuilder(); InputSource is = new InputSource(new StringReader(xmlString)); xmlDoc = builder.parse(is); } catch (SAXException | IOException | ParserConfigurationException e) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index fbae302bca6a..7edd3f9d01c5 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -32,6 +32,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.xerces.impl.dv.util.Base64; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -397,9 +398,7 @@ private Integer countDocument(String bearer, String query) { String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); @@ -436,9 +435,7 @@ private List searchDocumentIds(String bearer, String query, int s String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); @@ -489,9 +486,7 @@ private List searchDocument(String bearer, String id, String docTy private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); List namespaces = Arrays.asList(Namespace.getNamespace("ns", "http://www.epo.org/exchange")); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java index 000ef19eaec5..7718e59e483e 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java @@ -24,6 +24,7 @@ import com.google.common.io.CharStreams; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.utils.URIBuilder; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -233,11 +234,13 @@ private String getSingleElementValue(String src, String elementName) { String value = null; try { - SAXBuilder saxBuilder = new SAXBuilder(); - // Disallow external entities & entity expansion to protect against XXE attacks - // (NOTE: We receive errors if we disable all DTDs for PubMed, so this is the best we can do) - saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false); - saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); + // To properly parse PubMed responses, we must allow DOCTYPE/DTDs overall but + // we can still take advantage of entities themselves being disabled, and not + // expanded. + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); + saxBuilder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", + true); Document document = saxBuilder.build(new StringReader(src)); Element root = document.getRootElement(); @@ -354,12 +357,7 @@ public Collection call() throws Exception { private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // Disallow external entities & entity expansion to protect against XXE attacks - // (NOTE: We receive errors if we disable all DTDs for PubMed, so this is the best we can do) - saxBuilder.setFeature("http://xml.org/sax/features/external-general-entities", false); - saxBuilder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); - saxBuilder.setExpandEntities(false); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java index 7cd297eb2815..24f40339ddec 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java @@ -24,6 +24,7 @@ import org.apache.http.client.utils.URIBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -292,9 +293,7 @@ public Integer count(String query) throws URISyntaxException, ClientProtocolExce Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, buildURI(1, query), params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); Element element = root.getChild("hitCount"); @@ -365,9 +364,7 @@ public List search(String query, Integer size, Integer start) thro String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); String cursorMark = StringUtils.EMPTY; if (StringUtils.isNotBlank(response)) { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); XPathFactory xpfac = XPathFactory.instance(); XPathExpression xPath = xpfac.compile("//responseWrapper/resultList/result", @@ -419,4 +416,4 @@ public void setUrl(String url) { this.url = url; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java index a3f74694becf..22e3534ca89f 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java @@ -26,6 +26,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -208,9 +209,7 @@ public Integer call() throws Exception { return 0; } - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); @@ -397,9 +396,7 @@ private Map getRequestParameters(String query, String viewMode, private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); String totalResults = root.getChildText("totalResults", Namespace.getNamespace("http://a9.com/-/spec/opensearch/1.1/")); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java index 9bffa2a84a19..2ac63d50513f 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java @@ -26,6 +26,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.datamodel.Query; @@ -145,9 +146,7 @@ public Integer call() throws Exception { params.put(HEADER_PARAMETERS, getRequestParameters()); String response = liveImportClient.executeHttpGetRequest(timeout, url, params); - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); XPathExpression xpath = XPathFactory.instance().compile("//*[@name=\"RecordsFound\"]", @@ -288,9 +287,7 @@ private boolean isIsi(String query) { private List splitToRecords(String recordsSrc) { try { - SAXBuilder saxBuilder = new SAXBuilder(); - // disallow DTD parsing to ensure no XXE attacks can occur - saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true); + SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); String cData = XPathFactory.instance().compile("//*[@name=\"Records\"]", @@ -332,4 +329,4 @@ public void setApiKey(String apiKey) { this.apiKey = apiKey; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java index 1d777a2e13c8..3c9088bda5df 100644 --- a/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/license/CCLicenseConnectorServiceImpl.java @@ -28,6 +28,7 @@ import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.services.ConfigurationService; import org.jdom2.Attribute; import org.jdom2.Document; @@ -50,7 +51,7 @@ public class CCLicenseConnectorServiceImpl implements CCLicenseConnectorService, private Logger log = org.apache.logging.log4j.LogManager.getLogger(CCLicenseConnectorServiceImpl.class); private CloseableHttpClient client; - protected SAXBuilder parser = new SAXBuilder(); + protected SAXBuilder parser = XMLUtils.getSAXBuilder(); private String postArgument = "answers"; private String postAnswerFormat = diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java index 954336da2573..682e90828e36 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java @@ -43,6 +43,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicNameValuePair; import org.dspace.app.client.DSpaceHttpClientFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.orcid.OrcidToken; import org.dspace.orcid.exception.OrcidClientException; import org.dspace.orcid.model.OrcidEntityType; @@ -351,8 +352,7 @@ private String marshall(Object object) throws JAXBException { @SuppressWarnings("unchecked") private T unmarshall(HttpEntity entity, Class clazz) throws Exception { JAXBContext jaxbContext = JAXBContext.newInstance(clazz); - XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); - xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + XMLInputFactory xmlInputFactory = XMLUtils.getXMLInputFactory(); XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(entity.getContent()); Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); return (T) unmarshaller.unmarshal(xmlStreamReader); diff --git a/dspace-api/src/main/java/org/dspace/vocabulary/ControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/vocabulary/ControlledVocabulary.java index 7f2bdc6ef771..bd19a1254fe3 100644 --- a/dspace-api/src/main/java/org/dspace/vocabulary/ControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/vocabulary/ControlledVocabulary.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.List; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.xpath.XPath; @@ -20,6 +19,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.dspace.app.util.XMLUtils; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.w3c.dom.Document; @@ -71,7 +71,7 @@ public static ControlledVocabulary loadVocabulary(String fileName) File controlledVocFile = new File(filePath.toString()); if (controlledVocFile.exists()) { - DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + DocumentBuilder builder = XMLUtils.getDocumentBuilder(); Document document = builder.parse(controlledVocFile); XPath xPath = XPathFactory.newInstance().newXPath(); Node node = (Node) xPath.compile("node").evaluate(document, XPathConstants.NODE); From 8c80b67b04028a747515c84f0fbd1ef8ecd1ee40 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 2 Jul 2025 15:30:04 -0500 Subject: [PATCH 173/701] EPO and PubMed only need to allow for DOCTYPEs. All other XML security changes can be used. --- .../service/EpoImportMetadataSourceServiceImpl.java | 4 ++++ .../service/PubmedImportMetadataSourceServiceImpl.java | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index 7edd3f9d01c5..552f607827a8 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -399,6 +399,10 @@ private Integer countDocument(String bearer, String query) { String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); + // To properly parse EPO responses, we must allow DOCTYPEs overall. But, we can still apply all the + // other default XXE protections, including disabling external entities and entity expansion. + // NOTE: we only need to allow DOCTYPEs for this initial API call. All other calls have them disabled. + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); Document document = saxBuilder.build(new StringReader(response)); Element root = document.getRootElement(); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java index 7718e59e483e..c870161bf9bd 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java @@ -235,12 +235,9 @@ private String getSingleElementValue(String src, String elementName) { try { SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); - // To properly parse PubMed responses, we must allow DOCTYPE/DTDs overall but - // we can still take advantage of entities themselves being disabled, and not - // expanded. + // To properly parse PubMed responses, we must allow DOCTYPEs overall. But, we can still apply all the + // other default XXE protections, including disabling external entities and entity expansion. saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); - saxBuilder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", - true); Document document = saxBuilder.build(new StringReader(src)); Element root = document.getRootElement(); @@ -358,6 +355,9 @@ public Collection call() throws Exception { private List splitToRecords(String recordsSrc) { try { SAXBuilder saxBuilder = XMLUtils.getSAXBuilder(); + // To properly parse PubMed responses, we must allow DOCTYPEs overall. But, we can still apply all the + // other default XXE protections, including disabling external entities and entity expansion. + saxBuilder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false); Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); From 99b2a630a75db4eafbd7f44bb75a503f48e6170f Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Thu, 10 Jul 2025 16:00:34 +0200 Subject: [PATCH 174/701] Allow trusted XML builder to enforce base path for entities --- .../org/dspace/app/util/DCInputsReader.java | 19 ++-- .../app/util/SubmissionConfigReader.java | 10 +- .../java/org/dspace/app/util/XMLUtils.java | 94 +++++++++++++++++-- 3 files changed, 99 insertions(+), 24 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index d2f3ac0dc5d9..293ceaac47d7 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java @@ -8,6 +8,7 @@ package org.dspace.app.util; import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -118,19 +119,17 @@ private void buildInputs(String fileName) formDefns = new HashMap>>>(); valuePairs = new HashMap>(); - String uri = "file:" + new File(fileName).getAbsolutePath(); + File inputFile = new File(fileName); + String inputFileDir = inputFile.toPath().normalize().getParent().toString(); + + String uri = "file:" + inputFile.getAbsolutePath(); try { - // This document builder factory will *not* disable external + // This document builder will *not* disable external // entities as they can be useful in managing large forms, but - // it is up to site administrators to validate the XML they are - // storing - DocumentBuilderFactory factory = XMLUtils.getTrustedDocumentBuilderFactory(); - factory.setValidating(false); - factory.setIgnoringComments(true); - factory.setIgnoringElementContentWhitespace(true); - - DocumentBuilder db = factory.newDocumentBuilder(); + // it will restrict them to be within the directory that the + // current input form XML file exists (or a sub-directory) + DocumentBuilder db = XMLUtils.getTrustedDocumentBuilder(inputFileDir); Document doc = db.parse(uri); doNodes(doc); checkValues(); diff --git a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java index 1914ded86b66..27138d1ba1ef 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java @@ -162,14 +162,8 @@ private void buildInputs(String fileName) throws SubmissionConfigReaderException try { // This document builder factory will *not* disable external // entities as they can be useful in managing large forms, but - // it is up to site administrators to validate the XML they are - // storing - DocumentBuilderFactory factory = XMLUtils.getTrustedDocumentBuilderFactory(); - factory.setValidating(false); - factory.setIgnoringComments(true); - factory.setIgnoringElementContentWhitespace(true); - - DocumentBuilder db = factory.newDocumentBuilder(); + // it will restrict them to the config dir containing submission definitions + DocumentBuilder db = XMLUtils.getTrustedDocumentBuilder(configDir); Document doc = db.parse(uri); doNodes(doc); } catch (FactoryConfigurationError fe) { diff --git a/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java b/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java index 389d53fe6da4..6b419a0485e8 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java +++ b/dspace-api/src/main/java/org/dspace/app/util/XMLUtils.java @@ -7,7 +7,13 @@ */ package org.dspace.app.util; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -18,6 +24,9 @@ import org.jdom2.input.SAXBuilder; import org.w3c.dom.Element; import org.w3c.dom.NodeList; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; /** * Simple class to read information from small XML using DOM manipulation @@ -211,27 +220,36 @@ public static DocumentBuilderFactory getDocumentBuilderFactory() } /** - * Initialize and return a javax DocumentBuilder with NO security + * Initialize and return a javax DocumentBuilder with less security * applied. This is intended only for internal, administrative/configuration * use where external entities and other dangerous features are actually - * purposefully included. + * purposefully included, but are only allowed from specified paths, e.g. + * dspace.dir or some other path specified by the java caller. * The method here is tiny, but may be expanded with other features like * whitespace handling, and calling this method name helps to document * the fact that the caller knows it is trusting the XML source / builder + *

+ * If no allowedPaths are passed, then all external entities are rejected * * @return document builder with no security features set - * @throws ParserConfigurationException + * @throws ParserConfigurationException if the builder can not be configured */ - public static DocumentBuilder getTrustedDocumentBuilder() + public static DocumentBuilder getTrustedDocumentBuilder(String... allowedPaths) throws ParserConfigurationException { - return getTrustedDocumentBuilderFactory().newDocumentBuilder(); + DocumentBuilderFactory factory = getTrustedDocumentBuilderFactory(); + factory.setValidating(false); + factory.setIgnoringComments(true); + factory.setIgnoringElementContentWhitespace(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(new PathRestrictedEntityResolver(allowedPaths)); + return factory.newDocumentBuilder(); } /** * Initialize and return the javax DocumentBuilder with some basic security applied * to avoid XXE attacks and other unwanted content inclusion * @return document builder for use in XML parsing - * @throws ParserConfigurationException + * @throws ParserConfigurationException if the builder can not be configured */ public static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException { @@ -284,4 +302,68 @@ public static XMLInputFactory getXMLInputFactory() { return xmlInputFactory; } + /** + * This entity resolver accepts one or more path strings in its + * constructor and throws a SAXException if the entity systemID + * is not within the allowed path (or a subdirectory). + * If no parameters are passed, then this effectively disallows + * any external entity resolution. + */ + public static class PathRestrictedEntityResolver implements EntityResolver { + private final List allowedBasePaths; + + public PathRestrictedEntityResolver(String... allowedBasePaths) { + this.allowedBasePaths = Arrays.asList(allowedBasePaths); + } + + @Override + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + + if (systemId == null) { + return null; + } + + String filePath; + if (systemId.startsWith("file://")) { + filePath = systemId.substring(7); + } else if (systemId.startsWith("file:")) { + filePath = systemId.substring(5); + } else if (!systemId.contains("://")) { + filePath = systemId; + } else { + throw new SAXException("External resources not allowed: " + systemId + + ". Only local file paths are permitted."); + } + + Path resolvedPath; + try { + resolvedPath = Paths.get(filePath).toAbsolutePath().normalize(); + } catch (Exception e) { + throw new SAXException("Invalid path: " + systemId, e); + } + + boolean isAllowed = false; + for (String basePath : allowedBasePaths) { + Path allowedPath = Paths.get(basePath).toAbsolutePath().normalize(); + if (resolvedPath.startsWith(allowedPath)) { + isAllowed = true; + break; + } + } + + if (!isAllowed) { + throw new SAXException("Access denied to path: " + resolvedPath); + } + + File file = resolvedPath.toFile(); + if (!file.exists() || !file.canRead()) { + throw new SAXException("File not found or not readable: " + resolvedPath); + } + + return new InputSource(new FileInputStream(file)); + } + } + + } From d48e22aff5a72f539cbf2a4a29981a9ee4a6cdc8 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 11 Jul 2025 14:01:37 -0500 Subject: [PATCH 175/701] Update LICENSES_THIRD_PARTY to prepare for 7.6.4 release. --- LICENSES_THIRD_PARTY | 245 +++++++++++++++++++++++-------------------- pom.xml | 4 +- 2 files changed, 133 insertions(+), 116 deletions(-) diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index e12c429121ac..304ee4e34302 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -21,18 +21,18 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Apache Software License, Version 2.0: * Ant-Contrib Tasks (ant-contrib:ant-contrib:1.0b3 - http://ant-contrib.sourceforge.net) - * AWS SDK for Java - Core (com.amazonaws:aws-java-sdk-core:1.12.780 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK for AWS KMS (com.amazonaws:aws-java-sdk-kms:1.12.780 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK for Amazon S3 (com.amazonaws:aws-java-sdk-s3:1.12.780 - https://aws.amazon.com/sdkforjava) - * JMES Path Query library (com.amazonaws:jmespath-java:1.12.780 - https://aws.amazon.com/sdkforjava) + * AWS SDK for Java - Core (com.amazonaws:aws-java-sdk-core:1.12.785 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK for AWS KMS (com.amazonaws:aws-java-sdk-kms:1.12.785 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK for Amazon S3 (com.amazonaws:aws-java-sdk-s3:1.12.785 - https://aws.amazon.com/sdkforjava) + * JMES Path Query library (com.amazonaws:jmespath-java:1.12.785 - https://aws.amazon.com/sdkforjava) * HPPC Collections (com.carrotsearch:hppc:0.8.1 - http://labs.carrotsearch.com/hppc.html/hppc) * com.drewnoakes:metadata-extractor (com.drewnoakes:metadata-extractor:2.19.0 - https://drewnoakes.com/code/exif/) * parso (com.epam:parso:2.0.14 - https://github.com/epam/parso) * Internet Time Utility (com.ethlo.time:itu:1.7.0 - https://github.com/ethlo/itu) * ClassMate (com.fasterxml:classmate:1.7.0 - https://github.com/FasterXML/java-classmate) - * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.18.2 - https://github.com/FasterXML/jackson) - * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.18.2 - https://github.com/FasterXML/jackson-core) - * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.18.2 - https://github.com/FasterXML/jackson) + * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.19.1 - https://github.com/FasterXML/jackson) + * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.19.1 - https://github.com/FasterXML/jackson-core) + * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.19.1 - https://github.com/FasterXML/jackson) * Jackson dataformat: CBOR (com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.17.2 - https://github.com/FasterXML/jackson-dataformats-binary) * Jackson dataformat: Smile (com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.15.2 - https://github.com/FasterXML/jackson-dataformats-binary) * Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.14.0 - https://github.com/FasterXML/jackson-dataformats-text) @@ -57,22 +57,22 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Google APIs Client Library for Java (com.google.api-client:google-api-client:1.35.2 - https://github.com/googleapis/google-api-java-client/google-api-client) * Google Analytics API v3-rev145-1.23.0 (com.google.apis:google-api-services-analytics:v3-rev145-1.23.0 - http://nexus.sonatype.org/oss-repository-hosting.html/google-api-services-analytics) * FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/) - * Gson (com.google.code.gson:gson:2.10.1 - https://github.com/google/gson/gson) + * Gson (com.google.code.gson:gson:2.11.0 - https://github.com/google/gson) * error-prone annotations (com.google.errorprone:error_prone_annotations:2.21.1 - https://errorprone.info/error_prone_annotations) * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess) * Guava: Google Core Libraries for Java (com.google.guava:guava:32.1.3-jre - https://github.com/google/guava) * Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) - * Google HTTP Client Library for Java (com.google.http-client:google-http-client:1.45.3 - https://github.com/googleapis/google-http-java-client/google-http-client) + * Google HTTP Client Library for Java (com.google.http-client:google-http-client:1.47.0 - https://github.com/googleapis/google-http-java-client/google-http-client) * Apache HTTP transport v2 for the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-apache-v2:1.42.0 - https://github.com/googleapis/google-http-java-client/google-http-client-apache-v2) - * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.43.3 - https://github.com/googleapis/google-http-java-client/google-http-client-gson) - * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.45.3 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson2) + * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.47.0 - https://github.com/googleapis/google-http-java-client/google-http-client-gson) + * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.47.0 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson2) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:2.8 - https://github.com/google/j2objc/) - * Google OAuth Client Library for Java (com.google.oauth-client:google-oauth-client:1.37.0 - https://github.com/googleapis/google-oauth-java-client/google-oauth-client) + * Google OAuth Client Library for Java (com.google.oauth-client:google-oauth-client:1.39.0 - https://github.com/googleapis/google-oauth-java-client/google-oauth-client) * ConcurrentLinkedHashMap (com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2 - http://code.google.com/p/concurrentlinkedhashmap) * libphonenumber (com.googlecode.libphonenumber:libphonenumber:8.11.1 - https://github.com/google/libphonenumber/) - * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.5 - https://jackcess.sourceforge.io) - * Jackcess Encrypt (com.healthmarketscience.jackcess:jackcess-encrypt:4.0.2 - http://jackcessencrypt.sf.net) + * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.8 - https://jackcess.sourceforge.io) + * Jackcess Encrypt (com.healthmarketscience.jackcess:jackcess-encrypt:4.0.3 - http://jackcessencrypt.sf.net) * json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) * json-path-assert (com.jayway.jsonpath:json-path-assert:2.9.0 - https://github.com/jayway/JsonPath) * Disruptor Framework (com.lmax:disruptor:3.4.2 - http://lmax-exchange.github.com/disruptor) @@ -81,11 +81,15 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * JsonSchemaValidator (com.networknt:json-schema-validator:1.0.76 - https://github.com/networknt/json-schema-validator) * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:7.9 - https://bitbucket.org/connect2id/nimbus-jose-jwt) * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.28 - https://bitbucket.org/connect2id/nimbus-jose-jwt) - * opencsv (com.opencsv:opencsv:5.10 - http://opencsv.sf.net) + * opencsv (com.opencsv:opencsv:5.11.1 - http://opencsv.sf.net) * java-libpst (com.pff:java-libpst:0.9.3 - https://github.com/rjohnsondev/java-libpst) * rome (com.rometools:rome:1.19.0 - http://rometools.com/rome) * rome-modules (com.rometools:rome-modules:1.19.0 - http://rometools.com/rome-modules) * rome-utils (com.rometools:rome-utils:1.19.0 - http://rometools.com/rome-utils) + * mockwebserver (com.squareup.okhttp3:mockwebserver:4.12.0 - https://square.github.io/okhttp/) + * okhttp (com.squareup.okhttp3:okhttp:4.12.0 - https://square.github.io/okhttp/) + * okio (com.squareup.okio:okio:3.6.0 - https://github.com/square/okio/) + * okio (com.squareup.okio:okio-jvm:3.6.0 - https://github.com/square/okio/) * T-Digest (com.tdunning:t-digest:3.1 - https://github.com/tdunning/t-digest) * config (com.typesafe:config:1.3.3 - https://github.com/lightbend/config) * ssl-config-core (com.typesafe:ssl-config-core_2.13:0.3.8 - https://github.com/lightbend/ssl-config) @@ -98,15 +102,15 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * scala-logging (com.typesafe.scala-logging:scala-logging_2.13:3.9.2 - https://github.com/lightbend/scala-logging) * JSON library from Android SDK (com.vaadin.external.google:android-json:0.0.20131108.vaadin1 - http://developer.android.com/sdk) * SparseBitSet (com.zaxxer:SparseBitSet:1.3 - https://github.com/brettwooldridge/SparseBitSet) - * Apache Commons BeanUtils (commons-beanutils:commons-beanutils:1.10.0 - https://commons.apache.org/proper/commons-beanutils) + * Apache Commons BeanUtils (commons-beanutils:commons-beanutils:1.11.0 - https://commons.apache.org/proper/commons-beanutils) * Apache Commons CLI (commons-cli:commons-cli:1.9.0 - https://commons.apache.org/proper/commons-cli/) - * Apache Commons Codec (commons-codec:commons-codec:1.17.2 - https://commons.apache.org/proper/commons-codec/) + * Apache Commons Codec (commons-codec:commons-codec:1.18.0 - https://commons.apache.org/proper/commons-codec/) * Apache Commons Collections (commons-collections:commons-collections:3.2.2 - http://commons.apache.org/collections/) * Commons Digester (commons-digester:commons-digester:2.1 - http://commons.apache.org/digester/) - * Apache Commons FileUpload (commons-fileupload:commons-fileupload:1.5 - https://commons.apache.org/proper/commons-fileupload/) - * Apache Commons IO (commons-io:commons-io:2.18.0 - https://commons.apache.org/proper/commons-io/) + * Commons FileUpload (commons-fileupload:commons-fileupload:1.2.1 - http://commons.apache.org/fileupload/) + * Apache Commons IO (commons-io:commons-io:2.19.0 - https://commons.apache.org/proper/commons-io/) * Commons Lang (commons-lang:commons-lang:2.6 - http://commons.apache.org/lang/) - * Apache Commons Logging (commons-logging:commons-logging:1.3.4 - https://commons.apache.org/proper/commons-logging/) + * Apache Commons Logging (commons-logging:commons-logging:1.3.5 - https://commons.apache.org/proper/commons-logging/) * Apache Commons Validator (commons-validator:commons-validator:1.9.0 - http://commons.apache.org/proper/commons-validator/) * GeoJson POJOs for Jackson (de.grundid.opendatalab:geojson-jackson:1.14 - https://github.com/opendatalab-de/geojson-jackson) * OpenAIRE Funders Model (eu.openaire:funders-model:2.0.0 - https://api.openaire.eu) @@ -115,30 +119,34 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Metrics Integration for Jetty 9.3 and higher (io.dropwizard.metrics:metrics-jetty9:4.1.5 - https://metrics.dropwizard.io/metrics-jetty9) * Metrics Integration with JMX (io.dropwizard.metrics:metrics-jmx:4.1.5 - https://metrics.dropwizard.io/metrics-jmx) * JVM Integration for Metrics (io.dropwizard.metrics:metrics-jvm:4.1.5 - https://metrics.dropwizard.io/metrics-jvm) - * io.grpc:grpc-api (io.grpc:grpc-api:1.69.0 - https://github.com/grpc/grpc-java) - * io.grpc:grpc-context (io.grpc:grpc-context:1.69.0 - https://github.com/grpc/grpc-java) + * io.grpc:grpc-api (io.grpc:grpc-api:1.73.0 - https://github.com/grpc/grpc-java) + * io.grpc:grpc-context (io.grpc:grpc-context:1.73.0 - https://github.com/grpc/grpc-java) * micrometer-core (io.micrometer:micrometer-core:1.9.17 - https://github.com/micrometer-metrics/micrometer) - * Netty/Buffer (io.netty:netty-buffer:4.1.117.Final - https://netty.io/netty-buffer/) * Netty/Buffer (io.netty:netty-buffer:4.1.99.Final - https://netty.io/netty-buffer/) - * Netty/Codec (io.netty:netty-codec:4.1.117.Final - https://netty.io/netty-codec/) + * Netty/Buffer (io.netty:netty-buffer:4.2.2.Final - https://netty.io/netty-buffer/) * Netty/Codec (io.netty:netty-codec:4.1.99.Final - https://netty.io/netty-codec/) + * Netty/Codec (io.netty:netty-codec:4.2.2.Final - https://netty.io/netty-codec/) + * Netty/Codec/Base (io.netty:netty-codec-base:4.2.2.Final - https://netty.io/netty-codec-base/) + * Netty/Codec/Compression (io.netty:netty-codec-compression:4.2.2.Final - https://netty.io/netty-codec-compression/) * Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.86.Final - https://netty.io/netty-codec-http/) * Netty/Codec/HTTP2 (io.netty:netty-codec-http2:4.1.86.Final - https://netty.io/netty-codec-http2/) + * Netty/Codec/Marshalling (io.netty:netty-codec-marshalling:4.2.2.Final - https://netty.io/netty-codec-marshalling/) + * Netty/Codec/Protobuf (io.netty:netty-codec-protobuf:4.2.2.Final - https://netty.io/netty-codec-protobuf/) * Netty/Codec/Socks (io.netty:netty-codec-socks:4.1.86.Final - https://netty.io/netty-codec-socks/) - * Netty/Common (io.netty:netty-common:4.1.117.Final - https://netty.io/netty-common/) * Netty/Common (io.netty:netty-common:4.1.99.Final - https://netty.io/netty-common/) - * Netty/Handler (io.netty:netty-handler:4.1.117.Final - https://netty.io/netty-handler/) + * Netty/Common (io.netty:netty-common:4.2.2.Final - https://netty.io/netty-common/) * Netty/Handler (io.netty:netty-handler:4.1.99.Final - https://netty.io/netty-handler/) + * Netty/Handler (io.netty:netty-handler:4.2.2.Final - https://netty.io/netty-handler/) * Netty/Handler/Proxy (io.netty:netty-handler-proxy:4.1.86.Final - https://netty.io/netty-handler-proxy/) * Netty/Resolver (io.netty:netty-resolver:4.1.99.Final - https://netty.io/netty-resolver/) * Netty/TomcatNative [BoringSSL - Static] (io.netty:netty-tcnative-boringssl-static:2.0.56.Final - https://github.com/netty/netty-tcnative/netty-tcnative-boringssl-static/) * Netty/TomcatNative [OpenSSL - Classes] (io.netty:netty-tcnative-classes:2.0.56.Final - https://github.com/netty/netty-tcnative/netty-tcnative-classes/) - * Netty/Transport (io.netty:netty-transport:4.1.117.Final - https://netty.io/netty-transport/) * Netty/Transport (io.netty:netty-transport:4.1.99.Final - https://netty.io/netty-transport/) + * Netty/Transport (io.netty:netty-transport:4.2.2.Final - https://netty.io/netty-transport/) * Netty/Transport/Classes/Epoll (io.netty:netty-transport-classes-epoll:4.1.99.Final - https://netty.io/netty-transport-classes-epoll/) * Netty/Transport/Native/Epoll (io.netty:netty-transport-native-epoll:4.1.99.Final - https://netty.io/netty-transport-native-epoll/) - * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.1.117.Final - https://netty.io/netty-transport-native-unix-common/) * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.1.99.Final - https://netty.io/netty-transport-native-unix-common/) + * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.2.2.Final - https://netty.io/netty-transport-native-unix-common/) * OpenCensus (io.opencensus:opencensus-api:0.31.1 - https://github.com/census-instrumentation/opencensus-java) * OpenCensus (io.opencensus:opencensus-contrib-http-util:0.31.1 - https://github.com/census-instrumentation/opencensus-java) * OpenTracing API (io.opentracing:opentracing-api:0.33.0 - https://github.com/opentracing/opentracing-java/opentracing-api) @@ -167,16 +175,17 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * JSR107 API and SPI (javax.cache:cache-api:1.1.1 - https://github.com/jsr107/jsr107spec) * javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/) * jdbm (jdbm:jdbm:1.0 - no url defined) - * Joda-Time (joda-time:joda-time:2.13.0 - https://www.joda.org/joda-time/) - * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.16.1 - https://bytebuddy.net/byte-buddy) + * Joda-Time (joda-time:joda-time:2.14.0 - https://www.joda.org/joda-time/) + * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.11.13 - https://bytebuddy.net/byte-buddy) + * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.12.18 - https://bytebuddy.net/byte-buddy) * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.11.13 - https://bytebuddy.net/byte-buddy-agent) * eigenbase-properties (net.hydromatic:eigenbase-properties:1.1.5 - http://github.com/julianhyde/eigenbase-properties) * json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.36.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) * "Java Concurrency in Practice" book annotations (net.jcip:jcip-annotations:1.0 - http://jcip.net/) * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) - * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.1 - https://urielch.github.io/) + * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.2 - https://urielch.github.io/) * JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) - * JSON Small and Fast Parser (net.minidev:json-smart:2.5.1 - https://urielch.github.io/) + * JSON Small and Fast Parser (net.minidev:json-smart:2.5.2 - https://urielch.github.io/) * Abdera Core (org.apache.abdera:abdera-core:1.1.3 - http://abdera.apache.org/abdera-core) * I18N Libraries (org.apache.abdera:abdera-i18n:1.1.3 - http://abdera.apache.org) * Apache Ant Core (org.apache.ant:ant:1.10.15 - https://ant.apache.org/) @@ -186,18 +195,18 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Calcite Linq4j (org.apache.calcite:calcite-linq4j:1.35.0 - https://calcite.apache.org) * Apache Calcite Avatica (org.apache.calcite.avatica:avatica-core:1.23.0 - https://calcite.apache.org/avatica) * Apache Calcite Avatica Metrics (org.apache.calcite.avatica:avatica-metrics:1.23.0 - https://calcite.apache.org/avatica) - * Apache Commons Collections (org.apache.commons:commons-collections4:4.4 - https://commons.apache.org/proper/commons-collections/) + * Apache Commons Collections (org.apache.commons:commons-collections4:4.5.0 - https://commons.apache.org/proper/commons-collections/) * Apache Commons Compress (org.apache.commons:commons-compress:1.27.1 - https://commons.apache.org/proper/commons-compress/) - * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.11.0 - https://commons.apache.org/proper/commons-configuration/) - * Apache Commons CSV (org.apache.commons:commons-csv:1.10.0 - https://commons.apache.org/proper/commons-csv/) + * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.12.0 - https://commons.apache.org/proper/commons-configuration/) + * Apache Commons CSV (org.apache.commons:commons-csv:1.14.0 - https://commons.apache.org/proper/commons-csv/) * Apache Commons DBCP (org.apache.commons:commons-dbcp2:2.13.0 - https://commons.apache.org/proper/commons-dbcp/) * Apache Commons Digester (org.apache.commons:commons-digester3:3.2 - http://commons.apache.org/digester/) * Apache Commons Exec (org.apache.commons:commons-exec:1.3 - http://commons.apache.org/proper/commons-exec/) * Apache Commons Exec (org.apache.commons:commons-exec:1.4.0 - https://commons.apache.org/proper/commons-exec/) * Apache Commons Lang (org.apache.commons:commons-lang3:3.17.0 - https://commons.apache.org/proper/commons-lang/) * Apache Commons Math (org.apache.commons:commons-math3:3.6.1 - http://commons.apache.org/proper/commons-math/) - * Apache Commons Pool (org.apache.commons:commons-pool2:2.12.0 - https://commons.apache.org/proper/commons-pool/) - * Apache Commons Text (org.apache.commons:commons-text:1.13.0 - https://commons.apache.org/proper/commons-text) + * Apache Commons Pool (org.apache.commons:commons-pool2:2.12.1 - https://commons.apache.org/proper/commons-pool/) + * Apache Commons Text (org.apache.commons:commons-text:1.13.1 - https://commons.apache.org/proper/commons-text) * Curator Client (org.apache.curator:curator-client:2.13.0 - http://curator.apache.org/curator-client) * Curator Framework (org.apache.curator:curator-framework:2.13.0 - http://curator.apache.org/curator-framework) * Curator Recipes (org.apache.curator:curator-recipes:2.13.0 - http://curator.apache.org/curator-recipes) @@ -214,7 +223,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5/) * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5-h2/) * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.12 - http://james.apache.org/mime4j/apache-mime4j-core) - * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.11 - http://james.apache.org/mime4j/apache-mime4j-dom) + * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.12 - http://james.apache.org/mime4j/apache-mime4j-dom) * Apache Jena - Libraries POM (org.apache.jena:apache-jena-libs:2.13.0 - http://jena.apache.org/apache-jena-libs/) * Apache Jena - ARQ (SPARQL 1.1 Query Engine) (org.apache.jena:jena-arq:2.13.0 - http://jena.apache.org/jena-arq/) * Apache Jena - Core (org.apache.jena:jena-core:2.13.0 - http://jena.apache.org/jena-core/) @@ -224,9 +233,9 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Kerby-kerb Util (org.apache.kerby:kerb-util:1.0.1 - http://directory.apache.org/kerby/kerby-kerb/kerb-util) * Kerby ASN1 Project (org.apache.kerby:kerby-asn1:1.0.1 - http://directory.apache.org/kerby/kerby-common/kerby-asn1) * Kerby PKIX Project (org.apache.kerby:kerby-pkix:1.0.1 - http://directory.apache.org/kerby/kerby-pkix) - * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-1.2-api/) - * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-api/) - * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-core/) + * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.25.0 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.0 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.0 - https://logging.apache.org/log4j/2.x/) * Apache Log4j JUL Adapter (org.apache.logging.log4j:log4j-jul:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-jul/) * Apache Log4j Layout for JSON template (org.apache.logging.log4j:log4j-layout-template-json:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-layout-template-json/) * Apache Log4j SLF4J Binding (org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/) @@ -254,45 +263,45 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Lucene Spatial Extras (org.apache.lucene:lucene-spatial-extras:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial-extras) * Lucene Spatial 3D (org.apache.lucene:lucene-spatial3d:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial3d) * Lucene Suggest (org.apache.lucene:lucene-suggest:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-suggest) - * Apache FontBox (org.apache.pdfbox:fontbox:2.0.33 - http://pdfbox.apache.org/) + * Apache FontBox (org.apache.pdfbox:fontbox:2.0.34 - http://pdfbox.apache.org/) * PDFBox JBIG2 ImageIO plugin (org.apache.pdfbox:jbig2-imageio:3.0.4 - https://www.apache.org/jbig2-imageio/) * Apache JempBox (org.apache.pdfbox:jempbox:1.8.17 - http://www.apache.org/pdfbox-parent/jempbox/) - * Apache PDFBox (org.apache.pdfbox:pdfbox:2.0.33 - https://www.apache.org/pdfbox-parent/pdfbox/) - * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:2.0.31 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) - * Apache XmpBox (org.apache.pdfbox:xmpbox:2.0.31 - https://www.apache.org/pdfbox-parent/xmpbox/) - * Apache POI - Common (org.apache.poi:poi:5.2.5 - https://poi.apache.org/) - * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.2.5 - https://poi.apache.org/) - * Apache POI (org.apache.poi:poi-ooxml-lite:5.2.5 - https://poi.apache.org/) - * Apache POI (org.apache.poi:poi-scratchpad:5.2.5 - https://poi.apache.org/) + * Apache PDFBox (org.apache.pdfbox:pdfbox:2.0.34 - https://www.apache.org/pdfbox-parent/pdfbox/) + * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:2.0.34 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) + * Apache XmpBox (org.apache.pdfbox:xmpbox:2.0.34 - https://www.apache.org/pdfbox-parent/xmpbox/) + * Apache POI - Common (org.apache.poi:poi:5.4.1 - https://poi.apache.org/) + * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.4.1 - https://poi.apache.org/) + * Apache POI (org.apache.poi:poi-ooxml-lite:5.4.1 - https://poi.apache.org/) + * Apache POI (org.apache.poi:poi-scratchpad:5.4.1 - https://poi.apache.org/) * Apache Solr Core (org.apache.solr:solr-core:8.11.4 - https://lucene.apache.org/solr-parent/solr-core) * Apache Solr Solrj (org.apache.solr:solr-solrj:8.11.4 - https://lucene.apache.org/solr-parent/solr-solrj) * Apache Standard Taglib Implementation (org.apache.taglibs:taglibs-standard-impl:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-impl) * Apache Standard Taglib Specification API (org.apache.taglibs:taglibs-standard-spec:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-spec) * Apache Thrift (org.apache.thrift:libthrift:0.9.2 - http://thrift.apache.org) - * Apache Tika core (org.apache.tika:tika-core:2.9.2 - https://tika.apache.org/) - * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:2.9.2 - https://tika.apache.org/tika-parser-apple-module/) - * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:2.9.2 - https://tika.apache.org/tika-parser-audiovideo-module/) - * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:2.9.2 - https://tika.apache.org/tika-parser-cad-module/) - * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:2.9.2 - https://tika.apache.org/tika-parser-code-module/) - * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:2.9.2 - https://tika.apache.org/tika-parser-crypto-module/) - * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:2.9.2 - https://tika.apache.org/tika-parser-digest-commons/) - * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:2.9.2 - https://tika.apache.org/tika-parser-font-module/) - * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:2.9.2 - https://tika.apache.org/tika-parser-html-module/) - * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:2.9.2 - https://tika.apache.org/tika-parser-image-module/) - * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:2.9.2 - https://tika.apache.org/tika-parser-mail-commons/) - * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:2.9.2 - https://tika.apache.org/tika-parser-mail-module/) - * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:2.9.2 - https://tika.apache.org/tika-parser-microsoft-module/) - * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:2.9.2 - https://tika.apache.org/tika-parser-miscoffice-module/) - * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:2.9.2 - https://tika.apache.org/tika-parser-news-module/) - * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:2.9.2 - https://tika.apache.org/tika-parser-ocr-module/) - * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:2.9.2 - https://tika.apache.org/tika-parser-pdf-module/) - * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:2.9.2 - https://tika.apache.org/tika-parser-pkg-module/) - * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:2.9.2 - https://tika.apache.org/tika-parser-text-module/) - * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:2.9.2 - https://tika.apache.org/tika-parser-webarchive-module/) - * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:2.9.2 - https://tika.apache.org/tika-parser-xml-module/) - * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:2.9.2 - https://tika.apache.org/tika-parser-xmp-commons/) - * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:2.9.2 - https://tika.apache.org/tika-parser-zip-commons/) - * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:2.9.2 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) + * Apache Tika core (org.apache.tika:tika-core:2.9.4 - https://tika.apache.org/) + * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:2.9.4 - https://tika.apache.org/tika-parser-apple-module/) + * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:2.9.4 - https://tika.apache.org/tika-parser-audiovideo-module/) + * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:2.9.4 - https://tika.apache.org/tika-parser-cad-module/) + * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:2.9.4 - https://tika.apache.org/tika-parser-code-module/) + * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:2.9.4 - https://tika.apache.org/tika-parser-crypto-module/) + * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:2.9.4 - https://tika.apache.org/tika-parser-digest-commons/) + * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:2.9.4 - https://tika.apache.org/tika-parser-font-module/) + * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:2.9.4 - https://tika.apache.org/tika-parser-html-module/) + * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:2.9.4 - https://tika.apache.org/tika-parser-image-module/) + * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:2.9.4 - https://tika.apache.org/tika-parser-mail-commons/) + * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:2.9.4 - https://tika.apache.org/tika-parser-mail-module/) + * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:2.9.4 - https://tika.apache.org/tika-parser-microsoft-module/) + * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:2.9.4 - https://tika.apache.org/tika-parser-miscoffice-module/) + * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:2.9.4 - https://tika.apache.org/tika-parser-news-module/) + * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:2.9.4 - https://tika.apache.org/tika-parser-ocr-module/) + * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:2.9.4 - https://tika.apache.org/tika-parser-pdf-module/) + * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:2.9.4 - https://tika.apache.org/tika-parser-pkg-module/) + * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:2.9.4 - https://tika.apache.org/tika-parser-text-module/) + * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:2.9.4 - https://tika.apache.org/tika-parser-webarchive-module/) + * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:2.9.4 - https://tika.apache.org/tika-parser-xml-module/) + * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:2.9.4 - https://tika.apache.org/tika-parser-xmp-commons/) + * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:2.9.4 - https://tika.apache.org/tika-parser-zip-commons/) + * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:2.9.4 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:9.0.83 - https://tomcat.apache.org/) * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:9.0.83 - https://tomcat.apache.org/) * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:9.0.83 - https://tomcat.apache.org/) @@ -301,7 +310,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache Velocity Tools - Generic tools (org.apache.velocity.tools:velocity-tools-generic:3.1 - https://velocity.apache.org/tools/devel/velocity-tools-generic/) * Axiom API (org.apache.ws.commons.axiom:axiom-api:1.2.22 - http://ws.apache.org/axiom/) * Abdera Model (FOM) Implementation (org.apache.ws.commons.axiom:fom-impl:1.2.22 - http://ws.apache.org/axiom/implementations/fom-impl/) - * XmlBeans (org.apache.xmlbeans:xmlbeans:5.2.0 - https://xmlbeans.apache.org/) + * XmlBeans (org.apache.xmlbeans:xmlbeans:5.3.0 - https://xmlbeans.apache.org/) * Apache ZooKeeper - Server (org.apache.zookeeper:zookeeper:3.6.2 - http://zookeeper.apache.org/zookeeper) * Apache ZooKeeper - Jute (org.apache.zookeeper:zookeeper-jute:3.6.2 - http://zookeeper.apache.org/zookeeper-jute) * org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) @@ -348,9 +357,9 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * flyway-core (org.flywaydb:flyway-core:8.5.13 - https://flywaydb.org/flyway-core) * Ogg and Vorbis for Java, Core (org.gagravarr:vorbis-java-core:0.8 - https://github.com/Gagravarr/VorbisJava) * Apache Tika plugin for Ogg, Vorbis and FLAC (org.gagravarr:vorbis-java-tika:0.8 - https://github.com/Gagravarr/VorbisJava) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:6.2.5.Final - http://hibernate.org/validator/hibernate-validator) * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:6.2.5.Final - http://hibernate.org/validator/hibernate-validator-cdi) * org.immutables.value-annotations (org.immutables:value-annotations:2.9.2 - http://immutables.org/value-annotations) @@ -360,6 +369,11 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Java Annotation Indexer (org.jboss:jandex:2.4.2.Final - http://www.jboss.org/jandex) * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.6.1.Final - http://www.jboss.org) * JDOM (org.jdom:jdom2:2.0.6.1 - http://www.jdom.org) + * IntelliJ IDEA Annotations (org.jetbrains:annotations:13.0 - http://www.jetbrains.org) + * Kotlin Stdlib (org.jetbrains.kotlin:kotlin-stdlib:1.8.21 - https://kotlinlang.org/) + * Kotlin Stdlib Common (org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 - https://kotlinlang.org/) + * Kotlin Stdlib Jdk7 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 - https://kotlinlang.org/) + * Kotlin Stdlib Jdk8 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 - https://kotlinlang.org/) * jtwig-core (org.jtwig:jtwig-core:5.87.0.RELEASE - http://jtwig.org) * jtwig-reflection (org.jtwig:jtwig-reflection:5.87.0.RELEASE - http://jtwig.org) * jtwig-spring (org.jtwig:jtwig-spring:5.87.0.RELEASE - http://jtwig.org) @@ -377,8 +391,9 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jetty Servlet Tester (org.mortbay.jetty:jetty-servlet-tester:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-servlet-tester) * Jetty Utilities (org.mortbay.jetty:jetty-util:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-util) * Servlet Specification API (org.mortbay.jetty:servlet-api:2.5-20081211 - http://jetty.mortbay.org/servlet-api) - * jwarc (org.netpreserve:jwarc:0.29.0 - https://github.com/iipc/jwarc) + * jwarc (org.netpreserve:jwarc:0.31.1 - https://github.com/iipc/jwarc) * Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis) + * org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) * parboiled-core (org.parboiled:parboiled-core:1.1.7 - http://parboiled.org) * parboiled-java (org.parboiled:parboiled-java:1.1.7 - http://parboiled.org) * RRD4J (org.rrd4j:rrd4j:3.5 - https://github.com/rrd4j/rrd4j/) @@ -438,7 +453,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * SWORD v2 :: Common Server Library (org.swordapp:sword2-server:1.0 - http://www.swordapp.org/) * snappy-java (org.xerial.snappy:snappy-java:1.1.10.1 - https://github.com/xerial/snappy-java) * xml-matchers (org.xmlmatchers:xml-matchers:0.10 - http://code.google.com/p/xml-matchers/) - * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.0 - https://www.xmlunit.org/) + * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.2 - https://www.xmlunit.org/) * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.9.1 - https://www.xmlunit.org/) * org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.9.1 - https://www.xmlunit.org/xmlunit-placeholders/) * SnakeYAML (org.yaml:snakeyaml:1.30 - https://bitbucket.org/snakeyaml/snakeyaml) @@ -456,15 +471,15 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.15.0 - https://developers.google.com/protocol-buffers/protobuf-java/) * JZlib (com.jcraft:jzlib:1.1.3 - http://www.jcraft.com/jzlib/) * jmustache (com.samskivert:jmustache:1.15 - http://github.com/samskivert/jmustache) - * dnsjava (dnsjava:dnsjava:3.6.2 - https://github.com/dnsjava/dnsjava) + * dnsjava (dnsjava:dnsjava:3.6.3 - https://github.com/dnsjava/dnsjava) * jaxen (jaxen:jaxen:2.0.0 - http://www.cafeconleche.org/jaxen/jaxen) * ANTLR 4 Runtime (org.antlr:antlr4-runtime:4.5.1-1 - http://www.antlr.org/antlr4-runtime) * commons-compiler (org.codehaus.janino:commons-compiler:3.1.8 - http://janino-compiler.github.io/commons-compiler/) * janino (org.codehaus.janino:janino:3.1.8 - http://janino-compiler.github.io/janino/) * Stax2 API (org.codehaus.woodstox:stax2-api:4.2.1 - http://github.com/FasterXML/stax2-api) * Hamcrest Date (org.exparity:hamcrest-date:2.0.8 - https://github.com/exparity/hamcrest-date) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * Hamcrest (org.hamcrest:hamcrest:2.2 - http://hamcrest.org/JavaHamcrest/) * Hamcrest Core (org.hamcrest:hamcrest-core:2.2 - http://hamcrest.org/JavaHamcrest/) * HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.12 - http://hdrhistogram.github.io/HdrHistogram/) @@ -474,9 +489,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * asm-commons (org.ow2.asm:asm-commons:9.3 - http://asm.ow2.io/) * ASM Tree (org.ow2.asm:asm-tree:5.0.3 - http://asm.objectweb.org/asm-tree/) * ASM Util (org.ow2.asm:asm-util:5.0.3 - http://asm.objectweb.org/asm-util/) - * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.5 - https://jdbc.postgresql.org) + * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.7 - https://jdbc.postgresql.org) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) * JMatIO (org.tallison:jmatio:1.5 - https://github.com/tballison/jmatio) + * XZ for Java (org.tukaani:xz:1.10 - https://tukaani.org/xz/java.html) * XMLUnit for Java (xmlunit:xmlunit:1.3 - http://xmlunit.sourceforge.net/) CC0: @@ -495,7 +511,6 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * JavaBeans Activation Framework (JAF) (javax.activation:activation:1.1 - http://java.sun.com/products/javabeans/jaf/index.jsp) * JavaBeans Activation Framework API jar (javax.activation:javax.activation-api:1.2.0 - http://java.net/all/javax.activation-api/) * javax.annotation API (javax.annotation:javax.annotation-api:1.3.2 - http://jcp.org/en/jsr/detail?id=250) - * Expression Language 3.0 API (javax.el:javax.el-api:3.0.0 - http://uel-spec.java.net) * Java Servlet API (javax.servlet:javax.servlet-api:3.1.0 - http://servlet-spec.java.net) * javax.transaction API (javax.transaction:javax.transaction-api:1.3 - http://jta-spec.java.net) * jaxb-api (javax.xml.bind:jaxb-api:2.3.1 - https://github.com/javaee/jaxb-spec/jaxb-api) @@ -506,8 +521,8 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) * javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:jakarta.inject:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/external/jakarta.inject) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * Java Transaction API (org.jboss.spec.javax.transaction:jboss-transaction-api_1.2_spec:1.1.1.Final - http://www.jboss.org/jboss-transaction-api_1.2_spec) Cordra (Version 2) License Agreement: @@ -528,8 +543,8 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * javax.persistence-api (javax.persistence:javax.persistence-api:2.2 - https://github.com/javaee/jpa-spec) * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:2.3.9 - https://eclipse-ee4j.github.io/jaxb-ri/) * TXW2 Runtime (org.glassfish.jaxb:txw2:2.3.9 - https://eclipse-ee4j.github.io/jaxb-ri/) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * Java Persistence API, Version 2.1 (org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final - http://hibernate.org) * org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core) * org.locationtech.jts.io:jts-io-common (org.locationtech.jts.io:jts-io-common:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-io/jts-io-common) @@ -584,10 +599,13 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) * javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:jakarta.inject:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/external/jakarta.inject) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * Java Persistence API, Version 2.1 (org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final - http://hibernate.org) + * JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.4 - https://junit.org/junit5/) + * JUnit Platform Engine API (org.junit.platform:junit-platform-engine:1.11.4 - https://junit.org/junit5/) + * JUnit Vintage Engine (org.junit.vintage:junit-vintage-engine:5.11.4 - https://junit.org/junit5/) * org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core) * org.locationtech.jts.io:jts-io-common (org.locationtech.jts.io:jts-io-common:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-io/jts-io-common) * Jetty Server (org.mortbay.jetty:jetty:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/modules/jetty) @@ -596,11 +614,11 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines GENERAL PUBLIC LICENSE, version 3 (GPL-3.0): - * juniversalchardet (com.github.albfernandez:juniversalchardet:2.4.0 - https://github.com/albfernandez/juniversalchardet) + * juniversalchardet (com.github.albfernandez:juniversalchardet:2.5.0 - https://github.com/albfernandez/juniversalchardet) GNU LESSER GENERAL PUBLIC LICENSE, version 3 (LGPL-3.0): - * juniversalchardet (com.github.albfernandez:juniversalchardet:2.4.0 - https://github.com/albfernandez/juniversalchardet) + * juniversalchardet (com.github.albfernandez:juniversalchardet:2.5.0 - https://github.com/albfernandez/juniversalchardet) GNU Lesser General Public License (LGPL): @@ -626,9 +644,9 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * RE2/J (com.google.re2j:re2j:1.2 - http://github.com/google/re2j) - Handle.Net Public License Agreement (Ver.2): + Handle.Net Public License Agreement (Ver.3): - * Handle Server (net.handle:handle:9.3.1 - https://www.handle.net) + * Handle Server (net.handle:handle:9.3.2 - https://www.handle.net) ISC License: @@ -643,15 +661,15 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * s3mock (io.findify:s3mock_2.13:0.2.6 - https://github.com/findify/s3mock) * ClassGraph (io.github.classgraph:classgraph:4.8.154 - https://github.com/classgraph/classgraph) * JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) - * Bouncy Castle S/MIME API (org.bouncycastle:bcmail-jdk18on:1.77 - https://www.bouncycastle.org/java.html) - * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.80 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.80 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.80 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle JavaMail S/MIME APIs (org.bouncycastle:bcmail-jdk18on:1.80 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) * org.brotli:dec (org.brotli:dec:0.1.2 - http://brotli.org/dec) * Checker Qual (org.checkerframework:checker-qual:3.23.0 - https://checkerframework.org) - * Checker Qual (org.checkerframework:checker-qual:3.48.3 - https://checkerframework.org/) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * Checker Qual (org.checkerframework:checker-qual:3.49.3 - https://checkerframework.org/) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * mockito-core (org.mockito:mockito-core:3.12.4 - https://github.com/mockito/mockito) * mockito-inline (org.mockito:mockito-inline:3.12.4 - https://github.com/mockito/mockito) * ORCID - Model (org.orcid:orcid-model:3.0.2 - http://github.com/ORCID/orcid-model) @@ -664,27 +682,26 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * jquery (org.webjars.bowergithub.jquery:jquery-dist:3.7.1 - https://www.webjars.org) * urijs (org.webjars.bowergithub.medialize:uri.js:1.19.11 - https://www.webjars.org) * bootstrap (org.webjars.bowergithub.twbs:bootstrap:4.6.2 - https://www.webjars.org) - * core-js (org.webjars.npm:core-js:3.40.0 - https://www.webjars.org) - * @json-editor/json-editor (org.webjars.npm:json-editor__json-editor:2.15.1 - https://www.webjars.org) + * core-js (org.webjars.npm:core-js:3.42.0 - https://www.webjars.org) + * @json-editor/json-editor (org.webjars.npm:json-editor__json-editor:2.15.2 - https://www.webjars.org) Mozilla Public License: - * juniversalchardet (com.github.albfernandez:juniversalchardet:2.4.0 - https://github.com/albfernandez/juniversalchardet) + * juniversalchardet (com.github.albfernandez:juniversalchardet:2.5.0 - https://github.com/albfernandez/juniversalchardet) * H2 Database Engine (com.h2database:h2:2.3.232 - https://h2database.com) - * Saxon-HE (net.sf.saxon:Saxon-HE:9.8.0-14 - http://www.saxonica.com/) + * Saxon-HE (net.sf.saxon:Saxon-HE:9.9.1-8 - http://www.saxonica.com/) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) * Mozilla Rhino (org.mozilla:rhino:1.7.7.2 - https://developer.mozilla.org/en/Rhino) Public Domain: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.12 - http://hdrhistogram.github.io/HdrHistogram/) * JSON in Java (org.json:json:20231013 - https://github.com/douglascrockford/JSON-java) * LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) - * XZ for Java (org.tukaani:xz:1.9 - https://tukaani.org/xz/java.html) UnRar License: @@ -696,10 +713,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines W3C license: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) jQuery license: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.46 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) diff --git a/pom.xml b/pom.xml index 85fb3ce6ccd7..8a5c266b84b0 100644 --- a/pom.xml +++ b/pom.xml @@ -706,7 +706,7 @@ Apache Software License, Version 2.0|The SAX License|The W3C License Apache Software License, Version 2.0|Similar to Apache License but with the acknowledgment clause removed - BSD License|The BSD License|BSD licence|BSD license|BSD|BSD-style license|New BSD License|New BSD license|Revised BSD License|BSD 2-Clause license|3-Clause BSD License|BSD 2-Clause|BSD 3-clause New License|BSD Licence 3|BSD-2-Clause|BSD-3-Clause|Modified BSD|The New BSD License|The BSD 3-Clause License (BSD3)|BSD License 3|BSD License 2.0|The (New) BSD License + BSD License|The BSD License|BSD licence|BSD license|BSD|BSD-style license|New BSD License|New BSD license|Revised BSD License|BSD 2-Clause license|3-Clause BSD License|BSD 2-Clause|BSD 3-clause New License|BSD Licence 3|BSD-2-Clause|BSD-3-Clause|Modified BSD|The New BSD License|The BSD 3-Clause License (BSD3)|BSD License 3|BSD License 2.0|The (New) BSD License|0BSD BSD License|DSpace BSD License|DSpace Sourcecode License @@ -727,7 +727,7 @@ Common Development and Distribution License (CDDL)|GNU General Public License, Version 2 with the Classpath Exception Eclipse Distribution License, Version 1.0|Eclipse Distribution License (EDL), Version 1.0|Eclipse Distribution License - v 1.0|Eclipse Distribution License v. 1.0|EDL 1.0 - Eclipse Public License|Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0|EPL 1.0 license|Eclipse Public License (EPL), Version 1.0|Eclipse Public License 1.0|Eclipse Public License v1.0|Eclipse Public License, Version 1.0|EPL 1.0|EPL 2.0|Eclipse Public License - v 2.0|EPL-2.0|Eclipse Public License 2.0|Eclipse Public License v. 2.0|Eclipse Public License, Version 2.0 + Eclipse Public License|Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0|EPL 1.0 license|Eclipse Public License (EPL), Version 1.0|Eclipse Public License 1.0|Eclipse Public License v1.0|Eclipse Public License, Version 1.0|EPL 1.0|EPL 2.0|Eclipse Public License - v 2.0|EPL-2.0|Eclipse Public License 2.0|Eclipse Public License v. 2.0|Eclipse Public License, Version 2.0|Eclipse Public License v2.0 Eclipse Public License|Common Public License Version 1.0 From 259c3ddd370521bca4c5ebe77d02c1801c12b7dd Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sun, 13 Jul 2025 10:06:33 +0200 Subject: [PATCH 176/701] Enforce path traversal check on import subdir (pre-processing) --- .../app/itemimport/ItemImportServiceImpl.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java index 3af383b04caf..4c53dc6ee59f 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java @@ -29,6 +29,7 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.net.URL; +import java.nio.file.Path; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -744,15 +745,20 @@ protected Item addItem(Context c, List mycollections, String path, myitem = wi.getItem(); } + // normalize and validate path to make sure itemname doesn't contain path traversal + Path itemPath = new File(path + File.separatorChar + itemname + File.separatorChar) + .toPath().normalize(); + if (!itemPath.startsWith(path)) { + throw new IOException("Illegal item metadata path: '" + itemPath); + } + // now fill out dublin core for item - loadMetadata(c, myitem, path + File.separatorChar + itemname - + File.separatorChar); + loadMetadata(c, myitem, itemPath.toString()); // and the bitstreams from the contents file // process contents file, add bistreams and bundles, return any // non-standard permissions - List options = processContentsFile(c, myitem, path - + File.separatorChar + itemname, "contents"); + List options = processContentsFile(c, myitem, itemPath.toString(), "contents"); if (useWorkflow) { // don't process handle file @@ -770,8 +776,7 @@ protected Item addItem(Context c, List mycollections, String path, } } else { // only process handle file if not using workflow system - String myhandle = processHandleFile(c, myitem, path - + File.separatorChar + itemname, "handle"); + String myhandle = processHandleFile(c, myitem, itemPath.toString(), "handle"); // put item in system if (!isTest) { From 802a39fb77468bb9a0a30521579d00b15b9fed2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 03:33:08 +0000 Subject: [PATCH 177/701] Bump log4j.version from 2.24.3 to 2.25.1 Bumps `log4j.version` from 2.24.3 to 2.25.1. Updates `org.apache.logging.log4j:log4j-api` from 2.24.3 to 2.25.1 Updates `org.apache.logging.log4j:log4j-core` from 2.24.3 to 2.25.1 Updates `org.apache.logging.log4j:log4j-slf4j2-impl` from 2.24.3 to 2.25.1 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-version: 2.25.1 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.1 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.logging.log4j:log4j-slf4j2-impl dependency-version: 2.25.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e6f580426266..b0f3ae43199b 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 1.1.1 9.4.57.v20241219 - 2.24.3 + 2.25.1 2.0.34 1.19.0 2.0.17 From a381611ebd040dc4f88bda6cb66efdfd66366f7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 03:45:03 +0000 Subject: [PATCH 178/701] Bump log4j.version from 2.25.0 to 2.25.1 Bumps `log4j.version` from 2.25.0 to 2.25.1. Updates `org.apache.logging.log4j:log4j-api` from 2.25.0 to 2.25.1 Updates `org.apache.logging.log4j:log4j-core` from 2.25.0 to 2.25.1 Updates `org.apache.logging.log4j:log4j-1.2-api` from 2.25.0 to 2.25.1 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-version: 2.25.1 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.1 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-1.2-api dependency-version: 2.25.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8a5c266b84b0..d5d8b26225cf 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 1.1.1 9.4.57.v20241219 - 2.25.0 + 2.25.1 2.0.34 1.19.0 1.7.36 From 45a9f8b530d69a29de5c6e071ce76ebcb2837812 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 14 Jul 2025 12:48:55 +0200 Subject: [PATCH 179/701] Re-add file separator to normalized SAF item path --- .../org/dspace/app/itemimport/ItemImportServiceImpl.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java index 4c53dc6ee59f..6536af7139d4 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java @@ -751,14 +751,16 @@ protected Item addItem(Context c, List mycollections, String path, if (!itemPath.startsWith(path)) { throw new IOException("Illegal item metadata path: '" + itemPath); } + // Normalization chops off the last separator, and we need to put it back + String itemPathDir = itemPath.toString() + File.separatorChar; // now fill out dublin core for item - loadMetadata(c, myitem, itemPath.toString()); + loadMetadata(c, myitem, itemPathDir); // and the bitstreams from the contents file // process contents file, add bistreams and bundles, return any // non-standard permissions - List options = processContentsFile(c, myitem, itemPath.toString(), "contents"); + List options = processContentsFile(c, myitem, itemPathDir, "contents"); if (useWorkflow) { // don't process handle file @@ -776,7 +778,7 @@ protected Item addItem(Context c, List mycollections, String path, } } else { // only process handle file if not using workflow system - String myhandle = processHandleFile(c, myitem, itemPath.toString(), "handle"); + String myhandle = processHandleFile(c, myitem, itemPathDir, "handle"); // put item in system if (!isTest) { From dda6d9ec9ddf0697c3e51fa0bf70068eabb626e0 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 14 Jul 2025 13:06:08 +0200 Subject: [PATCH 180/701] Remove unused imports --- .../src/main/java/org/dspace/app/util/DCInputsReader.java | 2 -- .../main/java/org/dspace/app/util/SubmissionConfigReader.java | 1 - 2 files changed, 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index 293ceaac47d7..7c0ad4830f13 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java @@ -8,7 +8,6 @@ package org.dspace.app.util; import java.io.File; -import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -16,7 +15,6 @@ import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import org.apache.commons.lang3.StringUtils; diff --git a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java index 27138d1ba1ef..899702757adf 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import org.apache.commons.lang3.StringUtils; From e9bc74cf6df686965d988c1737c6803fe308bd9d Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 14 Jul 2025 13:09:43 +0200 Subject: [PATCH 181/701] Fix missing XMLUtils imports --- .../src/main/java/org/dspace/content/packager/RoleIngester.java | 1 + .../java/org/dspace/external/provider/orcid/xml/Converter.java | 1 + 2 files changed, 2 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java index d71012ff8356..5c4cf214445e 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java @@ -19,6 +19,7 @@ import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.codec.DecoderException; +import org.dspace.app.util.XMLUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; diff --git a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java index af4e31e7fae2..0e156f7ab5f3 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/Converter.java @@ -16,6 +16,7 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; +import org.dspace.app.util.XMLUtils; import org.xml.sax.SAXException; /** From 84e308c8f549b204eae89fbb36d6c2a3ef44a221 Mon Sep 17 00:00:00 2001 From: MMilosz Date: Fri, 4 Jul 2025 17:13:02 +0200 Subject: [PATCH 182/701] fix: prevent path traversal in SAF import (cherry picked from commit 596d8666f4b91c6af92f72f45da262d761b96607) --- .../app/itemimport/ItemImportServiceImpl.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java index 6536af7139d4..8f8e7438acbe 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportServiceImpl.java @@ -1010,6 +1010,34 @@ protected void addDCValue(Context c, Item i, String schema, Node n) } } + /** + * Ensures a file path does not attempt to access files outside the designated parent directory. + * + * @param parentDir The absolute path to the parent directory that should contain the file + * @param fileName The name or path of the file to validate + * @throws IOException If an error occurs while resolving canonical paths, or the file path attempts + * to access a location outside the parent directory + */ + private void validateFilePath(String parentDir, String fileName) throws IOException { + File parent = new File(parentDir); + File file = new File(fileName); + + // If the fileName is not an absolute path, we resolve it against the parentDir + if (!file.isAbsolute()) { + file = new File(parent, fileName); + } + + String parentCanonicalPath = parent.getCanonicalPath(); + String fileCanonicalPath = file.getCanonicalPath(); + + if (!fileCanonicalPath.startsWith(parentCanonicalPath)) { + log.error("File path outside of canonical root requested: fileCanonicalPath={} does not begin " + + "with parentCanonicalPath={}", fileCanonicalPath, parentCanonicalPath); + throw new IOException("Illegal file path '" + fileName + "' encountered. This references a path " + + "outside of the import package. Please see the system logs for more details."); + } + } + /** * Read the collections file inside the item directory. If there * is one and it is not empty return a list of collections in @@ -1210,6 +1238,7 @@ protected List processContentsFile(Context c, Item i, String path, sDescription = sDescription.replaceFirst("description:", ""); } + validateFilePath(path, sFilePath); registerBitstream(c, i, iAssetstore, sFilePath, sBundle, sDescription); logInfo("\tRegistering Bitstream: " + sFilePath + "\tAssetstore: " + iAssetstore @@ -1423,6 +1452,7 @@ protected void processContentFileEntry(Context c, Item i, String path, return; } + validateFilePath(path, fileName); String fullpath = path + File.separatorChar + fileName; // get an input stream From b0a4a3400f3285da033cc7f71b6ac80ec2e07a85 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 9 Jul 2025 13:59:37 +0200 Subject: [PATCH 183/701] Enforce bitstream path to be within (fs) bitstore base on get (cherry picked from commit 6799660a903bfea985c818654cca3891527780de) --- .../org/dspace/storage/bitstore/DSBitStoreService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java index 6fef7365e482..3f92ccaac5f1 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java @@ -12,6 +12,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -249,6 +250,12 @@ protected File getFile(Bitstream bitstream) throws IOException { log.debug("Local filename for " + sInternalId + " is " + bufFilename.toString()); } + File bitstreamFile = new File(bufFilename.toString()); + Path normalizedPath = bitstreamFile.toPath().normalize(); + if (!normalizedPath.startsWith(baseDir.getAbsolutePath())) { + log.error("Bitstream path outside of assetstore root requested: bitstream={}, path={}, assetstore={}", bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); + throw new IOException("Illegal bitstream path constructed"); + } return new File(bufFilename.toString()); } From 907b42c2a913ce748e71fbd15819e9b798909068 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 9 Jul 2025 19:07:32 +0200 Subject: [PATCH 184/701] return existing File constructed and validated for bitstream (cherry picked from commit 31b1c922b2cba42857679f291cb9320cf820db46) --- .../java/org/dspace/storage/bitstore/DSBitStoreService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java index 3f92ccaac5f1..6589f99bc749 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java @@ -256,7 +256,7 @@ protected File getFile(Bitstream bitstream) throws IOException { log.error("Bitstream path outside of assetstore root requested: bitstream={}, path={}, assetstore={}", bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); throw new IOException("Illegal bitstream path constructed"); } - return new File(bufFilename.toString()); + return bitstreamFile; } public boolean isRegisteredBitstream(String internalId) { From bc17559162125252c4c759ac540dffa982748c05 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 14 Jul 2025 12:42:30 +0200 Subject: [PATCH 185/701] Fix line length in DSBitstore log (cherry picked from commit dbf524c1120261f407cc77574642e3ee0e4ffe33) --- .../java/org/dspace/storage/bitstore/DSBitStoreService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java index 6589f99bc749..7743b93ca4ba 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java @@ -253,7 +253,9 @@ protected File getFile(Bitstream bitstream) throws IOException { File bitstreamFile = new File(bufFilename.toString()); Path normalizedPath = bitstreamFile.toPath().normalize(); if (!normalizedPath.startsWith(baseDir.getAbsolutePath())) { - log.error("Bitstream path outside of assetstore root requested: bitstream={}, path={}, assetstore={}", bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); + log.error("Bitstream path outside of assetstore root requested:" + + "bitstream={}, path={}, assetstore={}", + bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); throw new IOException("Illegal bitstream path constructed"); } return bitstreamFile; From a5f04f9c7731dbf088a00e28012367f24d76842b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 14 Jul 2025 12:03:12 -0500 Subject: [PATCH 186/701] [maven-release-plugin] prepare release dspace-7.6.4 --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 32 ++++++++++++++++---------------- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 7b3ab4a315e5..b6028cdb4241 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 24c3d5f1642c..080962f67760 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index b6c419702d05..9060c6a02f11 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index ee06135cdce4..951793b5f7f9 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index e7f2c20ec8e2..663ed04c4d32 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.6.4-SNAPSHOT + 7.6.4 DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 15ee58e6e1bc..ce71d1cad2e3 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 6d783349f165..3d55556936b2 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index fc436f668921..5e502e2b3fc2 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 755dc30d696b..97cf8e235f31 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index dd38555808a2..7687c54b921a 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index bc902913e967..798fb1045d13 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index c424668c8980..bd2b9a2128ff 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 318c01edd214..cbc2aee61ee1 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.6.4-SNAPSHOT + 7.6.4 .. diff --git a/dspace/pom.xml b/dspace/pom.xml index af6881303b96..d5ad247b3e24 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.6.4-SNAPSHOT + 7.6.4 ../pom.xml diff --git a/pom.xml b/pom.xml index 8a5c266b84b0..a87d28e8f6e2 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.6.4-SNAPSHOT + 7.6.4 DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -875,14 +875,14 @@ org.dspace dspace-rest - 7.6.4-SNAPSHOT + 7.6.4 jar classes org.dspace dspace-rest - 7.6.4-SNAPSHOT + 7.6.4 war @@ -1031,69 +1031,69 @@ org.dspace dspace-api - 7.6.4-SNAPSHOT + 7.6.4 org.dspace dspace-api test-jar - 7.6.4-SNAPSHOT + 7.6.4 test org.dspace.modules additions - 7.6.4-SNAPSHOT + 7.6.4 org.dspace dspace-sword - 7.6.4-SNAPSHOT + 7.6.4 org.dspace dspace-swordv2 - 7.6.4-SNAPSHOT + 7.6.4 org.dspace dspace-oai - 7.6.4-SNAPSHOT + 7.6.4 org.dspace dspace-services - 7.6.4-SNAPSHOT + 7.6.4 org.dspace dspace-server-webapp test-jar - 7.6.4-SNAPSHOT + 7.6.4 test org.dspace dspace-rdf - 7.6.4-SNAPSHOT + 7.6.4 org.dspace dspace-iiif - 7.6.4-SNAPSHOT + 7.6.4 org.dspace dspace-server-webapp - 7.6.4-SNAPSHOT + 7.6.4 jar classes org.dspace dspace-server-webapp - 7.6.4-SNAPSHOT + 7.6.4 war @@ -1939,7 +1939,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-7_x + dspace-7.6.4 From 9424ccf4aa6d81072391018063500c8b0f0714c0 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 14 Jul 2025 12:03:15 -0500 Subject: [PATCH 187/701] [maven-release-plugin] prepare for next development iteration --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 32 ++++++++++++++++---------------- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index b6028cdb4241..78acb3d23ae1 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 080962f67760..ff76244905d1 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 9060c6a02f11..af0ffdf2c772 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 951793b5f7f9..8a72bf049f30 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index 663ed04c4d32..bec3f96ee331 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.6.4 + 7.6.5-SNAPSHOT DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index ce71d1cad2e3..4b7ba616a650 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 3d55556936b2..2f4c4a5d3914 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 5e502e2b3fc2..c93949e1de56 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 97cf8e235f31..46a9c25890df 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 7687c54b921a..008c4f15a9a2 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 798fb1045d13..9da5e9d1059c 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index bd2b9a2128ff..a8bf8e6438c8 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index cbc2aee61ee1..18115c729743 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.6.4 + 7.6.5-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index d5ad247b3e24..5be9b100ffc1 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.6.4 + 7.6.5-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index a87d28e8f6e2..90703f43b1ad 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.6.4 + 7.6.5-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -875,14 +875,14 @@ org.dspace dspace-rest - 7.6.4 + 7.6.5-SNAPSHOT jar classes org.dspace dspace-rest - 7.6.4 + 7.6.5-SNAPSHOT war @@ -1031,69 +1031,69 @@ org.dspace dspace-api - 7.6.4 + 7.6.5-SNAPSHOT org.dspace dspace-api test-jar - 7.6.4 + 7.6.5-SNAPSHOT test org.dspace.modules additions - 7.6.4 + 7.6.5-SNAPSHOT org.dspace dspace-sword - 7.6.4 + 7.6.5-SNAPSHOT org.dspace dspace-swordv2 - 7.6.4 + 7.6.5-SNAPSHOT org.dspace dspace-oai - 7.6.4 + 7.6.5-SNAPSHOT org.dspace dspace-services - 7.6.4 + 7.6.5-SNAPSHOT org.dspace dspace-server-webapp test-jar - 7.6.4 + 7.6.5-SNAPSHOT test org.dspace dspace-rdf - 7.6.4 + 7.6.5-SNAPSHOT org.dspace dspace-iiif - 7.6.4 + 7.6.5-SNAPSHOT org.dspace dspace-server-webapp - 7.6.4 + 7.6.5-SNAPSHOT jar classes org.dspace dspace-server-webapp - 7.6.4 + 7.6.5-SNAPSHOT war @@ -1939,7 +1939,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-7.6.4 + dspace-7_x From 8f4e77170761c561b96a83051fadef1844fea624 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 14 Jul 2025 14:43:37 -0500 Subject: [PATCH 188/701] [maven-release-plugin] prepare for next development iteration --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/server-boot/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 28 ++++++++++++++-------------- 14 files changed, 27 insertions(+), 27 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 882f0b037c73..76458d7bba82 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 4a93ed41c006..6977ca4fd0ff 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 8a9bd8aa4044..407711f680aa 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index f18177116b45..5bc1c402bd7d 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index e38e99a30cb2..588aa7696266 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -14,7 +14,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 6d59839cdac8..8baf523a3b19 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 1866cd4c3a7b..00369160636e 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 29e81f2edb30..dc48a2759a82 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index e7c2073c73bf..a82710f88c99 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 1e9efb0401d1..7800d80026d9 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/server-boot/pom.xml b/dspace/modules/server-boot/pom.xml index 9aa952ec1290..6b73cc459dd3 100644 --- a/dspace/modules/server-boot/pom.xml +++ b/dspace/modules/server-boot/pom.xml @@ -11,7 +11,7 @@ modules org.dspace - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index a7399eee836a..48ecdfd0274b 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -7,7 +7,7 @@ modules org.dspace - 8.2 + 8.3-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index fd855be8b169..571ff5e371f0 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 8.2 + 8.3-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 38980f32b111..5ad544ff64ad 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 8.2 + 8.3-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -1016,68 +1016,68 @@ org.dspace dspace-api - 8.2 + 8.3-SNAPSHOT org.dspace dspace-api test-jar - 8.2 + 8.3-SNAPSHOT test org.dspace.modules additions - 8.2 + 8.3-SNAPSHOT org.dspace.modules server classes - 8.2 + 8.3-SNAPSHOT org.dspace dspace-sword - 8.2 + 8.3-SNAPSHOT org.dspace dspace-swordv2 - 8.2 + 8.3-SNAPSHOT org.dspace dspace-oai - 8.2 + 8.3-SNAPSHOT org.dspace dspace-services - 8.2 + 8.3-SNAPSHOT org.dspace dspace-server-webapp test-jar - 8.2 + 8.3-SNAPSHOT test org.dspace dspace-rdf - 8.2 + 8.3-SNAPSHOT org.dspace dspace-iiif - 8.2 + 8.3-SNAPSHOT org.dspace dspace-server-webapp - 8.2 + 8.3-SNAPSHOT @@ -1910,7 +1910,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-8.2 + dspace-8_x From 5cd3daa379b250b884c9093a358d18cf3b383588 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 02:49:30 +0000 Subject: [PATCH 189/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.785 to 1.12.788 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.785 to 1.12.788. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.785...1.12.788) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-version: 1.12.788 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 78acb3d23ae1..e4a86a0676fd 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -752,7 +752,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.785 + 1.12.788 From 271f50b3771d906a33aa45f366917d8967a45e6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 03:08:17 +0000 Subject: [PATCH 190/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.785 to 1.12.788 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.785 to 1.12.788. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.785...1.12.788) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-version: 1.12.788 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 76458d7bba82..6d1b0b8fc25d 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -731,7 +731,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.785 + 1.12.788 + test-argLine @@ -540,7 +540,7 @@ - -Xmx1024m + -Xmx1024m -Dfile.encoding=UTF-8 From 39fb2afba19b2b9b580dbac394a983958e8c5f27 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 16 Jul 2025 16:27:05 -0500 Subject: [PATCH 195/701] Update test to no longer assume 127.0.0.1 will always respond with "localhost" as the hostname. On my machine it does not. --- .../client/DSpaceHttpClientFactoryTest.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java b/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java index b518f19ff4d3..ca91bb5dc9ce 100644 --- a/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java +++ b/dspace-api/src/test/java/org/dspace/app/client/DSpaceHttpClientFactoryTest.java @@ -17,6 +17,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import java.net.InetAddress; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -107,7 +108,14 @@ public void testBuildWithProxyConfiguredAndHostToIgnoreSet() throws Exception { @Test public void testBuildWithProxyConfiguredAndHostPrefixToIgnoreSet() throws Exception { - setHttpProxyOnConfigurationService("local*", "www.test.com"); + // Get hostname assigned to 127.0.0.1 (usually is "localhost", but not always) + InetAddress address = InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); + String hostname = address.getHostName(); + // Take first 4 characters hostname as the prefix (e.g. "loca" in "localhost") + String hostnamePrefix = hostname.substring(0, 4); + // Save hostname prefix to our list of hosts to ignore, followed by an asterisk. + // (This should result in our Proxy ignoring our localhost) + setHttpProxyOnConfigurationService(hostnamePrefix + "*", "www.test.com"); CloseableHttpClient httpClient = httpClientFactory.build(); assertThat(mockProxy.getRequestCount(), is(0)); assertThat(mockServer.getRequestCount(), is(0)); @@ -122,7 +130,14 @@ public void testBuildWithProxyConfiguredAndHostPrefixToIgnoreSet() throws Except @Test public void testBuildWithProxyConfiguredAndHostSuffixToIgnoreSet() throws Exception { - setHttpProxyOnConfigurationService("www.test.com", "*host"); + // Get hostname assigned to 127.0.0.1 (usually is "localhost", but not always) + InetAddress address = InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); + String hostname = address.getHostName(); + // Take last 4 characters hostname as the suffix (e.g. "host" in "localhost") + String hostnameSuffix = hostname.substring(hostname.length() - 4); + // Save hostname suffix to our list of hosts to ignore, preceded by an asterisk. + // (This should result in our Proxy ignoring our localhost) + setHttpProxyOnConfigurationService("www.test.com", "*" + hostnameSuffix); CloseableHttpClient httpClient = httpClientFactory.build(); assertThat(mockProxy.getRequestCount(), is(0)); assertThat(mockServer.getRequestCount(), is(0)); From d2c120bd8b0dd111281c70ab3dba7b51d2395330 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 18 Jul 2025 11:33:41 -0500 Subject: [PATCH 196/701] Fix broken tests on Windows by using Paths to split file path instead of regex. Also switch to in-memory s3mock because Windows cannot cleanup created files successfully. --- .../storage/bitstore/S3BitStoreServiceIT.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java b/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java index 6ea21eac8d6d..63d88e950116 100644 --- a/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java @@ -26,9 +26,11 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -41,7 +43,6 @@ import com.amazonaws.services.s3.model.Bucket; import com.amazonaws.services.s3.model.ObjectMetadata; import io.findify.s3mock.S3Mock; -import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.BooleanUtils; import org.dspace.AbstractIntegrationTestWithDatabase; @@ -80,8 +81,6 @@ public class S3BitStoreServiceIT extends AbstractIntegrationTestWithDatabase { private Collection collection; - private File s3Directory; - private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @@ -89,9 +88,8 @@ public class S3BitStoreServiceIT extends AbstractIntegrationTestWithDatabase { public void setup() throws Exception { configurationService.setProperty("assetstore.s3.enabled", "true"); - s3Directory = new File(System.getProperty("java.io.tmpdir"), "s3"); - s3Mock = S3Mock.create(8001, s3Directory.getAbsolutePath()); + s3Mock = new S3Mock.Builder().withPort(8001).withInMemoryBackend().build(); s3Mock.start(); amazonS3Client = createAmazonS3Client(); @@ -112,8 +110,7 @@ public void setup() throws Exception { } @After - public void cleanUp() throws IOException { - FileUtils.deleteDirectory(s3Directory); + public void cleanUp() { s3Mock.shutdown(); } @@ -337,7 +334,7 @@ public void givenBitStreamIdentifierWhenIntermediatePathIsComputedThenNotEndingD String computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); int slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("2"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); @@ -362,31 +359,31 @@ public void givenBitStreamIdentidierWhenIntermediatePathIsComputedThenMustBeSpli String computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); int slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("2"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("3"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("4"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); path.append("56789"); computedPath = this.s3BitStoreService.getIntermediatePath(path.toString()); slashes = computeSlashes(path.toString()); assertThat(computedPath, Matchers.endsWith(File.separator)); - assertThat(computedPath.split(File.separator).length, Matchers.equalTo(slashes)); + assertThat(countPathElements(computedPath), Matchers.equalTo(slashes)); } @Test @@ -465,4 +462,12 @@ private int computeSlashes(String internalId) { return Math.min(slashes, S3BitStoreService.directoryLevels); } + // Count the number of elements in a Unix or Windows path. + // We use 'Paths' instead of splitting on slashes because these OSes use different path separators. + private int countPathElements(String stringPath) { + List pathElements = new ArrayList<>(); + Paths.get(stringPath).forEach(p -> pathElements.add(p.toString())); + return pathElements.size(); + } + } From 28686fc48adc8f6f188231c6531c27e58134548e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 21 Jul 2025 15:57:34 -0500 Subject: [PATCH 197/701] Improve logging in AbstractLiveImportIntegrationTest (to make tests easier to debug). Replace obscure StringInputStream (from Ant) with IOUtils.toInputStream --- .../importer/external/metadatamapping/MetadatumDTO.java | 9 +++++++++ .../app/rest/AbstractLiveImportIntegrationTest.java | 7 ++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/MetadatumDTO.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/MetadatumDTO.java index 265dd55eb933..b8a7507eaa61 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/MetadatumDTO.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/MetadatumDTO.java @@ -105,4 +105,13 @@ public String getValue() { public void setValue(String value) { this.value = value; } + + /** + * Return string representation of MetadatumDTO + * @return string representation of format "[schema].[element].[qualifier]=[value]" + */ + @Override + public String toString() { + return schema + "." + element + "." + qualifier + "=" + value; + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java index cf3e125cc531..94fbce4a5115 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java @@ -16,12 +16,12 @@ import java.util.ArrayList; import java.util.List; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.ProtocolVersion; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.entity.BasicHttpEntity; -import org.apache.tools.ant.filters.StringInputStream; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.importer.external.datamodel.ImportRecord; import org.dspace.importer.external.metadatamapping.MetadatumDTO; @@ -43,7 +43,8 @@ protected void matchRecords(ArrayList recordsImported, ArrayList list, List list2) { assertEquals(list.size(), list2.size()); for (int i = 0; i < list.size(); i++) { - assertTrue(sameMetadatum(list.get(i), list2.get(i))); + assertTrue("'" + list.get(i).toString() + "' should be equal to '" + list2.get(i).toString() + "'", + sameMetadatum(list.get(i), list2.get(i))); } } @@ -70,7 +71,7 @@ protected CloseableHttpResponse mockResponse(String xmlExample, int statusCode, throws UnsupportedEncodingException { BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); basicHttpEntity.setChunked(true); - basicHttpEntity.setContent(new StringInputStream(xmlExample)); + basicHttpEntity.setContent(IOUtils.toInputStream(xmlExample)); CloseableHttpResponse response = mock(CloseableHttpResponse.class); when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); From 15b3f314a3bd19cd47d8964f24385e8a08bca4c2 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 21 Jul 2025 15:58:37 -0500 Subject: [PATCH 198/701] Force UTF-8 encoding in all tests. This fixes several test failures when running tests from Windows commandline because Windows doesn't default to using UTF-8. --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 90703f43b1ad..3df4347fe92a 100644 --- a/pom.xml +++ b/pom.xml @@ -515,10 +515,10 @@ - + test-argLine @@ -527,7 +527,7 @@ - -Xmx1024m + -Xmx1024m -Dfile.encoding=UTF-8 From 588c4ef4d259bccb927440058639a00ace67fb11 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 22 Jul 2025 17:27:13 +0200 Subject: [PATCH 199/701] Add simple log4j WebappLoggingIT test --- .../dspace/app/rest/test/WebappLoggingIT.java | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java new file mode 100644 index 000000000000..fe746452c792 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java @@ -0,0 +1,101 @@ +/** + * 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.app.rest.test; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.junit.After; +import org.junit.Test; + +/** + * Test basic log4j logging functionality, extending AbstractControllerIntegrationTest + * purely to make sure we are testing the *web application* and not just the kernel + * as that is where logging has broken in the past. + * + * @author Kim Shepherd + */ +public class WebappLoggingIT extends AbstractControllerIntegrationTest { + + private static final Logger logger = LogManager.getLogger(WebappLoggingIT.class); + private static final String APPENDER_NAME = "DSpaceTestAppender"; + + static class InMemoryAppender extends AbstractAppender { + private final List messages = new ArrayList<>(); + + protected InMemoryAppender(String name) { + super( + name, + null, + PatternLayout.newBuilder().withPattern("%m").build(), + false, + Property.EMPTY_ARRAY + ); + start(); + } + + @Override + public void append(LogEvent event) { + messages.add(event.getMessage().getFormattedMessage()); + } + + public List getMessages() { + return messages; + } + } + + @Test + public void testLogging() throws Exception { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration config = context.getConfiguration(); + + InMemoryAppender appender = new InMemoryAppender(APPENDER_NAME); + config.addAppender(appender); + + LoggerConfig testLoggerConfig = new LoggerConfig(logger.getName(), Level.INFO, false); + testLoggerConfig.addAppender(appender, null, null); + config.addLogger(logger.getName(), testLoggerConfig); + context.updateLoggers(); + + logger.info("DSPACE TEST LOG ENTRY"); + + List messages = appender.getMessages(); + assertTrue(messages.stream().anyMatch(msg -> msg.contains("DSPACE TEST LOG ENTRY"))); + } + + @After + public void cleanupAppender() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration config = context.getConfiguration(); + + config.removeLogger(logger.getName()); + + Appender appender = config.getAppender(APPENDER_NAME); + if (appender != null) { + appender.stop(); + config.getAppenders().remove(APPENDER_NAME); + } + + context.updateLoggers(); +} + +} + From 479cb7688527eea88a87423540ec1c11f81f5848 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 22 Jul 2025 14:42:47 -0500 Subject: [PATCH 200/701] Minor updates to LICENSES_THIRD_PARTY for 7.6.5 release --- LICENSES_THIRD_PARTY | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index 304ee4e34302..92868467c989 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -233,9 +233,9 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Kerby-kerb Util (org.apache.kerby:kerb-util:1.0.1 - http://directory.apache.org/kerby/kerby-kerb/kerb-util) * Kerby ASN1 Project (org.apache.kerby:kerby-asn1:1.0.1 - http://directory.apache.org/kerby/kerby-common/kerby-asn1) * Kerby PKIX Project (org.apache.kerby:kerby-pkix:1.0.1 - http://directory.apache.org/kerby/kerby-pkix) - * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.25.0 - https://logging.apache.org/log4j/2.x/) - * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.0 - https://logging.apache.org/log4j/2.x/) - * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.0 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.25.1 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.1 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.1 - https://logging.apache.org/log4j/2.x/) * Apache Log4j JUL Adapter (org.apache.logging.log4j:log4j-jul:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-jul/) * Apache Log4j Layout for JSON template (org.apache.logging.log4j:log4j-layout-template-json:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-layout-template-json/) * Apache Log4j SLF4J Binding (org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/) From 6e8b6cc33acd3db77687995acbe3753d6ffdfc3a Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 22 Jul 2025 15:03:04 -0500 Subject: [PATCH 201/701] [maven-release-plugin] prepare release dspace-7.6.5 --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 32 ++++++++++++++++---------------- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 78acb3d23ae1..6071f64c6eac 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index ff76244905d1..79d8412c17b1 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index af0ffdf2c772..a46ee7ba5116 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 8a72bf049f30..d32a00bf6267 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index bec3f96ee331..64b798c58a12 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.6.5-SNAPSHOT + 7.6.5 DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 4b7ba616a650..193a9809e0dc 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 2f4c4a5d3914..7849381edfcf 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index c93949e1de56..cd5b8a6af30d 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 46a9c25890df..34a22026828a 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 008c4f15a9a2..125f0e16cceb 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 9da5e9d1059c..04ecf6c5efa7 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index a8bf8e6438c8..c182a7ab3b0a 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 18115c729743..56a2ecdfdaff 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.6.5-SNAPSHOT + 7.6.5 .. diff --git a/dspace/pom.xml b/dspace/pom.xml index 5be9b100ffc1..8e1060a432cb 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.6.5-SNAPSHOT + 7.6.5 ../pom.xml diff --git a/pom.xml b/pom.xml index 9f6c2ed42fbb..3526d2cf2e71 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.6.5-SNAPSHOT + 7.6.5 DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -875,14 +875,14 @@ org.dspace dspace-rest - 7.6.5-SNAPSHOT + 7.6.5 jar classes org.dspace dspace-rest - 7.6.5-SNAPSHOT + 7.6.5 war @@ -1031,69 +1031,69 @@ org.dspace dspace-api - 7.6.5-SNAPSHOT + 7.6.5 org.dspace dspace-api test-jar - 7.6.5-SNAPSHOT + 7.6.5 test org.dspace.modules additions - 7.6.5-SNAPSHOT + 7.6.5 org.dspace dspace-sword - 7.6.5-SNAPSHOT + 7.6.5 org.dspace dspace-swordv2 - 7.6.5-SNAPSHOT + 7.6.5 org.dspace dspace-oai - 7.6.5-SNAPSHOT + 7.6.5 org.dspace dspace-services - 7.6.5-SNAPSHOT + 7.6.5 org.dspace dspace-server-webapp test-jar - 7.6.5-SNAPSHOT + 7.6.5 test org.dspace dspace-rdf - 7.6.5-SNAPSHOT + 7.6.5 org.dspace dspace-iiif - 7.6.5-SNAPSHOT + 7.6.5 org.dspace dspace-server-webapp - 7.6.5-SNAPSHOT + 7.6.5 jar classes org.dspace dspace-server-webapp - 7.6.5-SNAPSHOT + 7.6.5 war @@ -1939,7 +1939,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-7_x + dspace-7.6.5 From c061015ecfbaa11fbb2681680f481e18e806ae2d Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 22 Jul 2025 15:03:08 -0500 Subject: [PATCH 202/701] [maven-release-plugin] prepare for next development iteration --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 32 ++++++++++++++++---------------- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6071f64c6eac..248a5727cbb4 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 79d8412c17b1..9cc05dc5ceb9 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index a46ee7ba5116..c9211d5e592d 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index d32a00bf6267..fc40a39d2a2e 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index 64b798c58a12..fa5c56003bf5 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.6.5 + 7.6.6-SNAPSHOT DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 193a9809e0dc..4cfe93f2ee45 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 7849381edfcf..26f6ecff3ef5 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index cd5b8a6af30d..15d918c4d106 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 34a22026828a..4972cd4e98d4 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 125f0e16cceb..41274052cd7c 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 04ecf6c5efa7..ecc5b3fec7a3 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index c182a7ab3b0a..4e7adb7b9224 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 56a2ecdfdaff..0e33182c698c 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.6.5 + 7.6.6-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index 8e1060a432cb..6338d764b515 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.6.5 + 7.6.6-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 3526d2cf2e71..5c7b514fc580 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.6.5 + 7.6.6-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -875,14 +875,14 @@ org.dspace dspace-rest - 7.6.5 + 7.6.6-SNAPSHOT jar classes org.dspace dspace-rest - 7.6.5 + 7.6.6-SNAPSHOT war @@ -1031,69 +1031,69 @@ org.dspace dspace-api - 7.6.5 + 7.6.6-SNAPSHOT org.dspace dspace-api test-jar - 7.6.5 + 7.6.6-SNAPSHOT test org.dspace.modules additions - 7.6.5 + 7.6.6-SNAPSHOT org.dspace dspace-sword - 7.6.5 + 7.6.6-SNAPSHOT org.dspace dspace-swordv2 - 7.6.5 + 7.6.6-SNAPSHOT org.dspace dspace-oai - 7.6.5 + 7.6.6-SNAPSHOT org.dspace dspace-services - 7.6.5 + 7.6.6-SNAPSHOT org.dspace dspace-server-webapp test-jar - 7.6.5 + 7.6.6-SNAPSHOT test org.dspace dspace-rdf - 7.6.5 + 7.6.6-SNAPSHOT org.dspace dspace-iiif - 7.6.5 + 7.6.6-SNAPSHOT org.dspace dspace-server-webapp - 7.6.5 + 7.6.6-SNAPSHOT jar classes org.dspace dspace-server-webapp - 7.6.5 + 7.6.6-SNAPSHOT war @@ -1939,7 +1939,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-7.6.5 + dspace-7_x From 4ce1f730335368a9281a6d26237e5791a4861d27 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 28 Nov 2024 17:38:20 +0100 Subject: [PATCH 203/701] 109807: Live Import - PubMed - support optional apiKey config (cherry picked from commit c25433ee17c318b1054933662f42090f2736eabf) --- ...PubmedImportMetadataSourceServiceImpl.java | 23 +++++++++++++++++++ .../spring-dspace-addon-import-services.xml | 1 + dspace/config/modules/external-providers.cfg | 5 +++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java index c870161bf9bd..dc9954969394 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java @@ -55,6 +55,7 @@ public class PubmedImportMetadataSourceServiceImpl extends AbstractImportMetadat private String urlFetch; private String urlSearch; + private String apiKey; private int attempt = 3; @@ -210,6 +211,9 @@ public GetNbRecords(Query query) { @Override public Integer call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("term", query.getParameterAsClass("query", String.class)); Map> params = new HashMap>(); @@ -286,6 +290,9 @@ public Collection call() throws Exception { List records = new LinkedList(); URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("retstart", start.toString()); uriBuilder.addParameter("retmax", count.toString()); @@ -316,6 +323,9 @@ public Collection call() throws Exception { String webEnv = getSingleElementValue(response, "WebEnv"); URIBuilder uriBuilder2 = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder2.addParameter("api_key", apiKey); + } uriBuilder2.addParameter("db", "pubmed"); uriBuilder2.addParameter("retstart", start.toString()); uriBuilder2.addParameter("retmax", count.toString()); @@ -388,6 +398,9 @@ public GetRecord(Query q) { public ImportRecord call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("retmode", "xml"); uriBuilder.addParameter("id", query.getParameterAsClass("id", String.class)); @@ -428,6 +441,9 @@ public FindMatchingRecords(Query q) { public Collection call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("usehistory", "y"); uriBuilder.addParameter("term", query.getParameterAsClass("term", String.class)); @@ -457,6 +473,9 @@ public Collection call() throws Exception { String queryKey = getSingleElementValue(response, "QueryKey"); URIBuilder uriBuilder2 = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder2.addParameter("db", "pubmed"); uriBuilder2.addParameter("retmode", "xml"); uriBuilder2.addParameter("WebEnv", webEnv); @@ -532,4 +551,8 @@ public void setUrlSearch(String urlSearch) { this.urlSearch = urlSearch; } + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + } diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index e693d26e538e..5cfef3bd22fb 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -70,6 +70,7 @@ + diff --git a/dspace/config/modules/external-providers.cfg b/dspace/config/modules/external-providers.cfg index f210a0aa5163..04f5c54e9848 100644 --- a/dspace/config/modules/external-providers.cfg +++ b/dspace/config/modules/external-providers.cfg @@ -45,6 +45,9 @@ epo.searchUrl = https://ops.epo.org/rest-services/published-data/search ################################################################# #---------------------- PubMed -----------------------------# #---------------------------------------------------------------# +# If apiKey is set then it's used, if not set or blank then it's not +# Max amount of requests per ip per second with apiKey is 10; without 3 +pubmed.apiKey = pubmed.url.search = https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi pubmed.url.fetch = https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi ################################################################# @@ -96,4 +99,4 @@ datacite.timeout = 180000 #---------------------------------------------------------------# ror.orgunit-import.api-url = https://api.ror.org/organizations -################################################################# \ No newline at end of file +################################################################# From 2f66457abe0a4a410e2dff67db2d376aec959a25 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 28 Nov 2024 17:38:20 +0100 Subject: [PATCH 204/701] 109807: Live Import - PubMed - support optional apiKey config --- ...PubmedImportMetadataSourceServiceImpl.java | 23 +++++++++++++++++++ .../spring-dspace-addon-import-services.xml | 1 + dspace/config/modules/external-providers.cfg | 3 +++ 3 files changed, 27 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java index c870161bf9bd..dc9954969394 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/service/PubmedImportMetadataSourceServiceImpl.java @@ -55,6 +55,7 @@ public class PubmedImportMetadataSourceServiceImpl extends AbstractImportMetadat private String urlFetch; private String urlSearch; + private String apiKey; private int attempt = 3; @@ -210,6 +211,9 @@ public GetNbRecords(Query query) { @Override public Integer call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("term", query.getParameterAsClass("query", String.class)); Map> params = new HashMap>(); @@ -286,6 +290,9 @@ public Collection call() throws Exception { List records = new LinkedList(); URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("retstart", start.toString()); uriBuilder.addParameter("retmax", count.toString()); @@ -316,6 +323,9 @@ public Collection call() throws Exception { String webEnv = getSingleElementValue(response, "WebEnv"); URIBuilder uriBuilder2 = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder2.addParameter("api_key", apiKey); + } uriBuilder2.addParameter("db", "pubmed"); uriBuilder2.addParameter("retstart", start.toString()); uriBuilder2.addParameter("retmax", count.toString()); @@ -388,6 +398,9 @@ public GetRecord(Query q) { public ImportRecord call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("retmode", "xml"); uriBuilder.addParameter("id", query.getParameterAsClass("id", String.class)); @@ -428,6 +441,9 @@ public FindMatchingRecords(Query q) { public Collection call() throws Exception { URIBuilder uriBuilder = new URIBuilder(urlSearch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder.addParameter("db", "pubmed"); uriBuilder.addParameter("usehistory", "y"); uriBuilder.addParameter("term", query.getParameterAsClass("term", String.class)); @@ -457,6 +473,9 @@ public Collection call() throws Exception { String queryKey = getSingleElementValue(response, "QueryKey"); URIBuilder uriBuilder2 = new URIBuilder(urlFetch); + if (StringUtils.isNotBlank(apiKey)) { + uriBuilder.addParameter("api_key", apiKey); + } uriBuilder2.addParameter("db", "pubmed"); uriBuilder2.addParameter("retmode", "xml"); uriBuilder2.addParameter("WebEnv", webEnv); @@ -532,4 +551,8 @@ public void setUrlSearch(String urlSearch) { this.urlSearch = urlSearch; } + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + } diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index c758966f449d..c3f1036382e1 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -56,6 +56,7 @@ + diff --git a/dspace/config/modules/external-providers.cfg b/dspace/config/modules/external-providers.cfg index b7c0e120dbcd..4f6de6e080ec 100644 --- a/dspace/config/modules/external-providers.cfg +++ b/dspace/config/modules/external-providers.cfg @@ -45,6 +45,9 @@ epo.searchUrl = https://ops.epo.org/rest-services/published-data/search ################################################################# #---------------------- PubMed -----------------------------# #---------------------------------------------------------------# +# If apiKey is set then it's used, if not set or blank then it's not +# Max amount of requests per ip per second with apiKey is 10; without 3 +pubmed.apiKey = pubmed.url.search = https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi pubmed.url.fetch = https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi ################################################################# From ffd60bf8115b6f6ce35167cbd52d9ea7040ba5be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 02:55:08 +0000 Subject: [PATCH 205/701] Bump the fasterxml group with 3 updates Bumps the fasterxml group with 3 updates: [com.fasterxml.jackson.core:jackson-annotations](https://github.com/FasterXML/jackson), [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) and [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson). Updates `com.fasterxml.jackson.core:jackson-annotations` from 2.19.1 to 2.19.2 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.core:jackson-core` from 2.19.1 to 2.19.2 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.19.1...jackson-core-2.19.2) Updates `com.fasterxml.jackson.core:jackson-core` from 2.19.1 to 2.19.2 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.19.1...jackson-core-2.19.2) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.19.1 to 2.19.2 - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-annotations dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 5c7b514fc580..b8f9dd44ee52 100644 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 3.10.8 2.31.0 - 2.19.1 - 2.19.1 + 2.19.2 + 2.19.2 1.3.2 2.3.1 2.3.9 From 609062befbd00027724561409cccb389f7e4019f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 02:55:24 +0000 Subject: [PATCH 206/701] Bump the google-apis group with 3 updates Bumps the google-apis group with 3 updates: [com.google.http-client:google-http-client](https://github.com/googleapis/google-http-java-client), [com.google.http-client:google-http-client-jackson2](https://github.com/googleapis/google-http-java-client) and [com.google.http-client:google-http-client-gson](https://github.com/googleapis/google-http-java-client). Updates `com.google.http-client:google-http-client` from 1.47.0 to 1.47.1 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.47.0...v1.47.1) Updates `com.google.http-client:google-http-client-jackson2` from 1.47.0 to 1.47.1 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.47.0...v1.47.1) Updates `com.google.http-client:google-http-client-gson` from 1.47.0 to 1.47.1 - [Release notes](https://github.com/googleapis/google-http-java-client/releases) - [Changelog](https://github.com/googleapis/google-http-java-client/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/google-http-java-client/compare/v1.47.0...v1.47.1) --- updated-dependencies: - dependency-name: com.google.http-client:google-http-client dependency-version: 1.47.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: google-apis - dependency-name: com.google.http-client:google-http-client-jackson2 dependency-version: 1.47.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: google-apis - dependency-name: com.google.http-client:google-http-client-gson dependency-version: 1.47.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: google-apis ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 5c7b514fc580..df92203466df 100644 --- a/pom.xml +++ b/pom.xml @@ -1701,7 +1701,7 @@ com.google.http-client google-http-client - 1.47.0 + 1.47.1 com.google.errorprone @@ -1723,7 +1723,7 @@ com.google.http-client google-http-client-jackson2 - 1.47.0 + 1.47.1 jackson-core @@ -1745,7 +1745,7 @@ com.google.http-client google-http-client-gson - 1.47.0 + 1.47.1 com.squareup.okhttp3 From 92120211da77d6c73cd948de274d813de66825b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 02:55:44 +0000 Subject: [PATCH 207/701] Bump the test-tools group across 1 directory with 7 updates --- updated-dependencies: - dependency-name: org.xmlunit:xmlunit-core dependency-version: 2.10.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-buffer dependency-version: 4.2.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport dependency-version: 4.2.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport-native-unix-common dependency-version: 4.2.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-common dependency-version: 4.2.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-handler dependency-version: 4.2.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-codec dependency-version: 4.2.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 248a5727cbb4..18be290ad01f 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -884,32 +884,32 @@ io.netty netty-buffer - 4.2.2.Final + 4.2.3.Final io.netty netty-transport - 4.2.2.Final + 4.2.3.Final io.netty netty-transport-native-unix-common - 4.2.2.Final + 4.2.3.Final io.netty netty-common - 4.2.2.Final + 4.2.3.Final io.netty netty-handler - 4.2.2.Final + 4.2.3.Final io.netty netty-codec - 4.2.2.Final + 4.2.3.Final org.apache.velocity @@ -919,7 +919,7 @@ org.xmlunit xmlunit-core - 2.10.2 + 2.10.3 test From d396e4968bced33e0070aa215c533a7390b270dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 03:09:34 +0000 Subject: [PATCH 208/701] Bump the fasterxml group with 4 updates Bumps the fasterxml group with 4 updates: [com.fasterxml.jackson.core:jackson-annotations](https://github.com/FasterXML/jackson), [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core), [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) and com.fasterxml.jackson.datatype:jackson-datatype-jsr310. Updates `com.fasterxml.jackson.core:jackson-annotations` from 2.19.1 to 2.19.2 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.core:jackson-core` from 2.19.1 to 2.19.2 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.19.1...jackson-core-2.19.2) Updates `com.fasterxml.jackson.core:jackson-core` from 2.19.1 to 2.19.2 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.19.1...jackson-core-2.19.2) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.19.1 to 2.19.2 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.19.1 to 2.19.2 Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.19.1 to 2.19.2 --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-annotations dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency-version: 2.19.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 50c57d5b9476..bf9bcc9bd013 100644 --- a/pom.xml +++ b/pom.xml @@ -31,8 +31,8 @@ 3.10.8 2.38.0 - 2.19.1 - 2.19.1 + 2.19.2 + 2.19.2 2.1.1 4.0.2 4.0.5 From 59e37eb381fb068934bee848850cd7dc6f7cc2a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 03:18:35 +0000 Subject: [PATCH 209/701] Bump org.xmlunit:xmlunit-core in the test-tools group Bumps the test-tools group with 1 update: [org.xmlunit:xmlunit-core](https://github.com/xmlunit/xmlunit). Updates `org.xmlunit:xmlunit-core` from 2.10.2 to 2.10.3 - [Release notes](https://github.com/xmlunit/xmlunit/releases) - [Changelog](https://github.com/xmlunit/xmlunit/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/xmlunit/xmlunit/compare/v2.10.2...v2.10.3) --- updated-dependencies: - dependency-name: org.xmlunit:xmlunit-core dependency-version: 2.10.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 76458d7bba82..d6c600ed45e5 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -785,7 +785,7 @@ org.xmlunit xmlunit-core - 2.10.2 + 2.10.3 test From ac81a3d36f7e3e0a24a9077ba0d5c03e9de23ac6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 03:36:02 +0000 Subject: [PATCH 210/701] Bump the apache-commons group across 1 directory with 5 updates Bumps the apache-commons group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [commons-codec:commons-codec](https://github.com/apache/commons-codec) | `1.18.0` | `1.19.0` | | [commons-io:commons-io](https://github.com/apache/commons-io) | `2.19.0` | `2.20.0` | | org.apache.commons:commons-lang3 | `3.17.0` | `3.18.0` | | [org.apache.commons:commons-text](https://github.com/apache/commons-text) | `1.13.1` | `1.14.0` | | commons-validator:commons-validator | `1.9.0` | `1.10.0` | Updates `commons-codec:commons-codec` from 1.18.0 to 1.19.0 - [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.18.0...rel/commons-codec-1.19.0) Updates `commons-io:commons-io` from 2.19.0 to 2.20.0 - [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.19.0...rel/commons-io-2.20.0) Updates `org.apache.commons:commons-lang3` from 3.17.0 to 3.18.0 Updates `org.apache.commons:commons-text` from 1.13.1 to 1.14.0 - [Changelog](https://github.com/apache/commons-text/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-text/compare/rel/commons-text-1.13.1...rel/commons-text-1.14.0) Updates `commons-validator:commons-validator` from 1.9.0 to 1.10.0 --- updated-dependencies: - dependency-name: commons-codec:commons-codec dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-io:commons-io dependency-version: 2.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-lang3 dependency-version: 3.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-text dependency-version: 1.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-validator:commons-validator dependency-version: 1.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 5c7b514fc580..9f94b8aab42d 100644 --- a/pom.xml +++ b/pom.xml @@ -1478,7 +1478,7 @@ commons-codec commons-codec - 1.18.0 + 1.19.0 org.apache.commons @@ -1498,12 +1498,12 @@ commons-io commons-io - 2.19.0 + 2.20.0 org.apache.commons commons-lang3 - 3.17.0 + 3.18.0 @@ -1525,12 +1525,12 @@ org.apache.commons commons-text - 1.13.1 + 1.14.0 commons-validator commons-validator - 1.9.0 + 1.10.0 joda-time From de220d55acb6b8f8b5f5f77a26089369e1ddd701 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 03:37:58 +0000 Subject: [PATCH 211/701] Bump org.apache.james:apache-mime4j-core from 0.8.12 to 0.8.13 Bumps org.apache.james:apache-mime4j-core from 0.8.12 to 0.8.13. --- updated-dependencies: - dependency-name: org.apache.james:apache-mime4j-core dependency-version: 0.8.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5c7b514fc580..54591d3a5f74 100644 --- a/pom.xml +++ b/pom.xml @@ -1324,7 +1324,7 @@ org.apache.james apache-mime4j-core - 0.8.12 + 0.8.13 From dc65c7a3046d89f6960ec7843cd704b801dd3aa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 03:39:24 +0000 Subject: [PATCH 212/701] Bump com.opencsv:opencsv from 5.11.1 to 5.12.0 Bumps com.opencsv:opencsv from 5.11.1 to 5.12.0. --- updated-dependencies: - dependency-name: com.opencsv:opencsv dependency-version: 5.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 248a5727cbb4..ab15840d20be 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -801,7 +801,7 @@ com.opencsv opencsv - 5.11.1 + 5.12.0 From c199df9d4f9f7278a8e88d5b408cb67dc0faf93c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 03:47:20 +0000 Subject: [PATCH 213/701] Bump com.opencsv:opencsv from 5.11.1 to 5.12.0 Bumps com.opencsv:opencsv from 5.11.1 to 5.12.0. --- updated-dependencies: - dependency-name: com.opencsv:opencsv dependency-version: 5.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 76458d7bba82..265f2e8cfc36 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -772,7 +772,7 @@ com.opencsv opencsv - 5.11.1 + 5.12.0 From db9c5e6599581b22571b513396d9d3c77a8ad249 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 03:49:26 +0000 Subject: [PATCH 214/701] Bump org.apache.james:apache-mime4j-core from 0.8.12 to 0.8.13 Bumps org.apache.james:apache-mime4j-core from 0.8.12 to 0.8.13. --- updated-dependencies: - dependency-name: org.apache.james:apache-mime4j-core dependency-version: 0.8.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 50c57d5b9476..5e58905860d5 100644 --- a/pom.xml +++ b/pom.xml @@ -1320,7 +1320,7 @@ org.apache.james apache-mime4j-core - 0.8.12 + 0.8.13 From 0cd33e09fab453b039bb26f6639444dc54b0ef7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 04:04:36 +0000 Subject: [PATCH 215/701] Bump the spring group across 1 directory with 25 updates Bumps the spring group with 25 updates in the / directory: | Package | From | To | | --- | --- | --- | | [org.springframework:spring-orm](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-core](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-beans](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-aop](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-context](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-context-support](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-tx](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-jdbc](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-web](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-expression](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework:spring-test](https://github.com/spring-projects/spring-framework) | `6.2.8` | `6.2.9` | | [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-tomcat](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-maven-plugin](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-cache](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-thymeleaf](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-web](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-data-rest](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-security](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-aop](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-actuator](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.boot:spring-boot-starter-log4j2](https://github.com/spring-projects/spring-boot) | `3.5.3` | `3.5.4` | | [org.springframework.security:spring-security-test](https://github.com/spring-projects/spring-security) | `6.5.1` | `6.5.2` | Updates `org.springframework:spring-orm` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-core` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-beans` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-aop` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-context` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-context-support` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-tx` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-jdbc` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-web` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-webmvc` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-expression` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-test` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-core` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-beans` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-aop` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-context` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-context-support` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-tx` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-jdbc` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-web` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-webmvc` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-expression` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework:spring-test` from 6.2.8 to 6.2.9 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.8...v6.2.9) Updates `org.springframework.boot:spring-boot-starter-test` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.security:spring-security-test` from 6.5.1 to 6.5.2 - [Release notes](https://github.com/spring-projects/spring-security/releases) - [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc) - [Commits](https://github.com/spring-projects/spring-security/compare/6.5.1...6.5.2) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.3...v3.5.4) --- updated-dependencies: - dependency-name: org.springframework:spring-orm dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.security:spring-security-test dependency-version: 6.5.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 50c57d5b9476..99bdbf258e94 100644 --- a/pom.xml +++ b/pom.xml @@ -19,9 +19,9 @@ 17 - 6.2.8 - 3.5.3 - 6.5.1 + 6.2.9 + 3.5.4 + 6.5.2 6.4.8.Final 8.0.2.Final 42.7.7 From 68613d2c5ab1ee57e7ccd40c52523974585f1d8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 04:34:13 +0000 Subject: [PATCH 216/701] Bump the apache-commons group across 1 directory with 5 updates Bumps the apache-commons group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [commons-codec:commons-codec](https://github.com/apache/commons-codec) | `1.18.0` | `1.19.0` | | [commons-io:commons-io](https://github.com/apache/commons-io) | `2.19.0` | `2.20.0` | | org.apache.commons:commons-lang3 | `3.17.0` | `3.18.0` | | [org.apache.commons:commons-text](https://github.com/apache/commons-text) | `1.13.1` | `1.14.0` | | commons-validator:commons-validator | `1.9.0` | `1.10.0` | Updates `commons-codec:commons-codec` from 1.18.0 to 1.19.0 - [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.18.0...rel/commons-codec-1.19.0) Updates `commons-io:commons-io` from 2.19.0 to 2.20.0 - [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.19.0...rel/commons-io-2.20.0) Updates `org.apache.commons:commons-lang3` from 3.17.0 to 3.18.0 Updates `org.apache.commons:commons-text` from 1.13.1 to 1.14.0 - [Changelog](https://github.com/apache/commons-text/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-text/compare/rel/commons-text-1.13.1...rel/commons-text-1.14.0) Updates `commons-validator:commons-validator` from 1.9.0 to 1.10.0 --- updated-dependencies: - dependency-name: commons-codec:commons-codec dependency-version: 1.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-io:commons-io dependency-version: 2.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-lang3 dependency-version: 3.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-text dependency-version: 1.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-validator:commons-validator dependency-version: 1.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 50c57d5b9476..747d083f07b8 100644 --- a/pom.xml +++ b/pom.xml @@ -1476,7 +1476,7 @@ commons-codec commons-codec - 1.18.0 + 1.19.0 org.apache.commons @@ -1503,12 +1503,12 @@ commons-io commons-io - 2.19.0 + 2.20.0 org.apache.commons commons-lang3 - 3.17.0 + 3.18.0 @@ -1535,12 +1535,12 @@ org.apache.commons commons-text - 1.13.1 + 1.14.0 commons-validator commons-validator - 1.9.0 + 1.10.0 jakarta.activation From ceab9526c414d1ead05f4c18d69b70b07d991ae6 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Mon, 28 Jul 2025 13:42:28 -0400 Subject: [PATCH 217/701] Port #11075 to 7_x --- .../authority/DSpaceControlledVocabulary.java | 89 +++++++++++-------- .../DSpaceControlledVocabularyTest.java | 2 + 2 files changed, 55 insertions(+), 36 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 444332df97d2..3f7f926c07a4 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -34,35 +34,37 @@ * from {@code ${dspace.dir}/config/controlled-vocabularies/*.xml} and turns * them into autocompleting authorities. * - * Configuration: This MUST be configured as a self-named plugin, e.g.: {@code - * plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \ + *

Configuration: This MUST be configured as a self-named plugin, e.g.: {@code + * plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = * org.dspace.content.authority.DSpaceControlledVocabulary * } * - * It AUTOMATICALLY configures a plugin instance for each XML file in the + *

It AUTOMATICALLY configures a plugin instance for each XML file in the * controlled vocabularies directory. The name of the plugin is the basename of * the file; e.g., {@code ${dspace.dir}/config/controlled-vocabularies/nsi.xml} * would generate a plugin called "nsi". * - * Each configured plugin comes with three configuration options: {@code - * vocabulary.plugin._plugin_.hierarchy.store = - * # Store entire hierarchy along with selected value. Default: TRUE - * vocabulary.plugin._plugin_.hierarchy.suggest = - * # Display entire hierarchy in the suggestion list. Default: TRUE - * vocabulary.plugin._plugin_.delimiter = "" - * # Delimiter to use when building hierarchy strings. Default: "::" - * } + *

Each configured plugin comes with three configuration options: + *

    + *
  • {@code vocabulary.plugin._plugin_.hierarchy.store = } + * # Store entire hierarchy along with selected value. Default: TRUE
  • + *
  • {@code vocabulary.plugin._plugin_.hierarchy.suggest = + * # Display entire hierarchy in the suggestion list. Default: TRUE}
  • + *
  • {@code vocabulary.plugin._plugin_.delimiter = "" + * # Delimiter to use when building hierarchy strings. Default: "::"}
  • + *
* * @author Michael B. Klein */ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceControlledVocabulary.class); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); protected static String xpathTemplate = "//node[contains(translate(@label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ'," + - "'abcdefghijklmnopqrstuvwxyz'),'%s')]"; - protected static String idTemplate = "//node[@id = '%s']"; - protected static String labelTemplate = "//node[@label = '%s']"; + "'abcdefghijklmnopqrstuvwxyz'),%s)]"; + protected static String idTemplate = "//node[@id = %s]"; + protected static String idTemplateQuoted = "//node[@id = '%s']"; + protected static String labelTemplate = "//node[@label = %s]"; protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy/parent::node"; protected static String rootTemplate = "/node"; protected static String pluginNames[] = null; @@ -106,7 +108,7 @@ public boolean accept(File dir, String name) { File.separator + "config" + File.separator + "controlled-vocabularies"; String[] xmlFiles = (new File(vocabulariesPath)).list(new xmlFilter()); - List names = new ArrayList(); + List names = new ArrayList<>(); for (String filename : xmlFiles) { names.add((new File(filename)).getName().replace(".xml", "")); } @@ -162,14 +164,23 @@ protected String buildString(Node node) { public Choices getMatches(String text, int start, int limit, String locale) { init(); log.debug("Getting matches for '" + text + "'"); - String xpathExpression = ""; String[] textHierarchy = text.split(hierarchyDelimiter, -1); + StringBuilder xpathExpressionBuilder = new StringBuilder(); for (int i = 0; i < textHierarchy.length; i++) { - xpathExpression += String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "'").toLowerCase()); + xpathExpressionBuilder.append(String.format(xpathTemplate, "$var" + i)); } + String xpathExpression = xpathExpressionBuilder.toString(); XPath xpath = XPathFactory.newInstance().newXPath(); - int total = 0; - List choices = new ArrayList(); + xpath.setXPathVariableResolver(variableName -> { + String varName = variableName.getLocalPart(); + if (varName.startsWith("var")) { + int index = Integer.parseInt(varName.substring(3)); + return textHierarchy[index].toLowerCase(); + } + throw new IllegalArgumentException("Unexpected variable: " + varName); + }); + int total; + List choices; try { NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); total = results.getLength(); @@ -185,14 +196,23 @@ public Choices getMatches(String text, int start, int limit, String locale) { @Override public Choices getBestMatch(String text, String locale) { init(); - log.debug("Getting best matches for '" + text + "'"); - String xpathExpression = ""; + log.debug("Getting best matches for {}'", text); String[] textHierarchy = text.split(hierarchyDelimiter, -1); + StringBuilder xpathExpressionBuilder = new StringBuilder(); for (int i = 0; i < textHierarchy.length; i++) { - xpathExpression += String.format(labelTemplate, textHierarchy[i].replaceAll("'", "'")); + xpathExpressionBuilder.append(String.format(labelTemplate, "$var" + i)); } + String xpathExpression = xpathExpressionBuilder.toString(); XPath xpath = XPathFactory.newInstance().newXPath(); - List choices = new ArrayList(); + xpath.setXPathVariableResolver(variableName -> { + String varName = variableName.getLocalPart(); + if (varName.startsWith("var")) { + int index = Integer.parseInt(varName.substring(3)); + return textHierarchy[index]; + } + throw new IllegalArgumentException("Unexpected variable: " + varName); + }); + List choices; try { NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); choices = getChoicesFromNodeList(results, 0, 1); @@ -240,7 +260,7 @@ public Choices getTopChoices(String authorityName, int start, int limit, String @Override public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) { init(); - String xpathExpression = String.format(idTemplate, parentId); + String xpathExpression = String.format(idTemplateQuoted, parentId); return getChoicesByXpath(xpathExpression, start, limit); } @@ -264,15 +284,12 @@ public Integer getPreloadLevel() { } private boolean isRootElement(Node node) { - if (node != null && node.getOwnerDocument().getDocumentElement().equals(node)) { - return true; - } - return false; + return node != null && node.getOwnerDocument().getDocumentElement().equals(node); } private Node getNode(String key) throws XPathExpressionException { init(); - String xpathExpression = String.format(idTemplate, key); + String xpathExpression = String.format(idTemplateQuoted, key); Node node = getNodeFromXPath(xpathExpression); return node; } @@ -284,7 +301,7 @@ private Node getNodeFromXPath(String xpathExpression) throws XPathExpressionExce } private List getChoicesFromNodeList(NodeList results, int start, int limit) { - List choices = new ArrayList(); + List choices = new ArrayList<>(); for (int i = 0; i < results.getLength(); i++) { if (i < start) { continue; @@ -303,14 +320,14 @@ private List getChoicesFromNodeList(NodeList results, int start, int lim private Map addOtherInformation(String parentCurr, String noteCurr, List childrenCurr, String authorityCurr) { - Map extras = new HashMap(); + Map extras = new HashMap<>(); if (StringUtils.isNotBlank(parentCurr)) { extras.put("parent", parentCurr); } if (StringUtils.isNotBlank(noteCurr)) { extras.put("note", noteCurr); } - if (childrenCurr.size() > 0) { + if (!childrenCurr.isEmpty()) { extras.put("hasChildren", "true"); } else { extras.put("hasChildren", "false"); @@ -368,7 +385,7 @@ private String getNote(Node node) { } private List getChildren(Node node) { - List children = new ArrayList(); + List children = new ArrayList<>(); NodeList childNodes = node.getChildNodes(); for (int ci = 0; ci < childNodes.getLength(); ci++) { Node firstChild = childNodes.item(ci); @@ -391,7 +408,7 @@ private List getChildren(Node node) { private boolean isSelectable(Node node) { Node selectableAttr = node.getAttributes().getNamedItem("selectable"); if (null != selectableAttr) { - return Boolean.valueOf(selectableAttr.getNodeValue()); + return Boolean.parseBoolean(selectableAttr.getNodeValue()); } else { // Default is true return true; } @@ -418,7 +435,7 @@ private String getAuthority(Node node) { } private Choices getChoicesByXpath(String xpathExpression, int start, int limit) { - List choices = new ArrayList(); + List choices = new ArrayList<>(); XPath xpath = XPathFactory.newInstance().newXPath(); try { Node parentNode = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); diff --git a/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java b/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java index 255b070e5eac..524c6407b7bd 100644 --- a/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java +++ b/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java @@ -8,6 +8,7 @@ package org.dspace.content.authority; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import java.io.IOException; @@ -86,6 +87,7 @@ public void testGetMatches() throws IOException, ClassNotFoundException { CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE), "farm"); assertNotNull(instance); Choices result = instance.getMatches(text, start, limit, locale); + assertNotEquals("At least one match expected", 0, result.values.length); assertEquals("north 40", result.values[0].value); } From 6ef8ea0dab01e9760e3dd691d62f2a9119185981 Mon Sep 17 00:00:00 2001 From: Zeroday BYTE Date: Thu, 17 Jul 2025 02:06:29 +0700 Subject: [PATCH 218/701] Update DSpaceControlledVocabulary.java (cherry picked from commit e94f0a9cb3d43c1ed7af44683202737f598681d5) --- .../authority/DSpaceControlledVocabulary.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 444332df97d2..390efdb7c6b0 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -162,12 +162,21 @@ protected String buildString(Node node) { public Choices getMatches(String text, int start, int limit, String locale) { init(); log.debug("Getting matches for '" + text + "'"); - String xpathExpression = ""; String[] textHierarchy = text.split(hierarchyDelimiter, -1); + StringBuilder xpathExpressionBuilder = new StringBuilder(); for (int i = 0; i < textHierarchy.length; i++) { - xpathExpression += String.format(xpathTemplate, textHierarchy[i].replaceAll("'", "'").toLowerCase()); + xpathExpressionBuilder.append(String.format(xpathTemplate, "$var" + i)); } + String xpathExpression = xpathExpressionBuilder.toString(); XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setXPathVariableResolver(variableName -> { + String varName = variableName.getLocalPart(); + if (varName.startsWith("var")) { + int index = Integer.parseInt(varName.substring(3)); + return textHierarchy[index].toLowerCase(); + } + throw new IllegalArgumentException("Unexpected variable: " + varName); + }); int total = 0; List choices = new ArrayList(); try { @@ -186,12 +195,21 @@ public Choices getMatches(String text, int start, int limit, String locale) { public Choices getBestMatch(String text, String locale) { init(); log.debug("Getting best matches for '" + text + "'"); - String xpathExpression = ""; String[] textHierarchy = text.split(hierarchyDelimiter, -1); + StringBuilder xpathExpressionBuilder = new StringBuilder(); for (int i = 0; i < textHierarchy.length; i++) { - xpathExpression += String.format(labelTemplate, textHierarchy[i].replaceAll("'", "'")); + xpathExpressionBuilder.append(String.format(labelTemplate, "$var" + i)); } + String xpathExpression = xpathExpressionBuilder.toString(); XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setXPathVariableResolver(variableName -> { + String varName = variableName.getLocalPart(); + if (varName.startsWith("var")) { + int index = Integer.parseInt(varName.substring(3)); + return textHierarchy[index]; + } + throw new IllegalArgumentException("Unexpected variable: " + varName); + }); List choices = new ArrayList(); try { NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); From b9a3ecf76484cdbed97529981516c98a3eb64106 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 24 Jul 2025 08:32:50 -0400 Subject: [PATCH 219/701] 'No match' should be test failure, not index error. (cherry picked from commit c781ba278093f7c80b8b68c601309fe90a9b85f0) --- .../content/authority/DSpaceControlledVocabularyTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java b/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java index 255b070e5eac..524c6407b7bd 100644 --- a/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java +++ b/dspace-api/src/test/java/org/dspace/content/authority/DSpaceControlledVocabularyTest.java @@ -8,6 +8,7 @@ package org.dspace.content.authority; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import java.io.IOException; @@ -86,6 +87,7 @@ public void testGetMatches() throws IOException, ClassNotFoundException { CoreServiceFactory.getInstance().getPluginService().getNamedPlugin(Class.forName(PLUGIN_INTERFACE), "farm"); assertNotNull(instance); Choices result = instance.getMatches(text, start, limit, locale); + assertNotEquals("At least one match expected", 0, result.values.length); assertEquals("north 40", result.values[0].value); } From 93c1d8f5aef8f56696307abc0dda89c339260364 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 24 Jul 2025 11:51:53 -0400 Subject: [PATCH 220/701] Variables in XPath expressions should not be quoted. Documentation cleanup. Clean up many IDE warnings. (cherry picked from commit 7deaf1cca5c35b721726be1fa7b8a0aa86647938) --- .../authority/DSpaceControlledVocabulary.java | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 390efdb7c6b0..48d7c6999a96 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -34,23 +34,25 @@ * from {@code ${dspace.dir}/config/controlled-vocabularies/*.xml} and turns * them into autocompleting authorities. * - * Configuration: This MUST be configured as a self-named plugin, e.g.: {@code - * plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \ + *

Configuration: This MUST be configured as a self-named plugin, e.g.: {@code + * plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = * org.dspace.content.authority.DSpaceControlledVocabulary * } * - * It AUTOMATICALLY configures a plugin instance for each XML file in the + *

It AUTOMATICALLY configures a plugin instance for each XML file in the * controlled vocabularies directory. The name of the plugin is the basename of * the file; e.g., {@code ${dspace.dir}/config/controlled-vocabularies/nsi.xml} * would generate a plugin called "nsi". * - * Each configured plugin comes with three configuration options: {@code - * vocabulary.plugin._plugin_.hierarchy.store = - * # Store entire hierarchy along with selected value. Default: TRUE - * vocabulary.plugin._plugin_.hierarchy.suggest = - * # Display entire hierarchy in the suggestion list. Default: TRUE - * vocabulary.plugin._plugin_.delimiter = "" - * # Delimiter to use when building hierarchy strings. Default: "::" + *

Each configured plugin comes with three configuration options: + *

    + *
  • {@code vocabulary.plugin._plugin_.hierarchy.store = + * # Store entire hierarchy along with selected value. Default: TRUE}
  • + *
  • {@code vocabulary.plugin._plugin_.hierarchy.suggest = + * # Display entire hierarchy in the suggestion list. Default: TRUE}
  • + *
  • {@code vocabulary.plugin._plugin_.delimiter = "" + * # Delimiter to use when building hierarchy strings. Default: "::"}
  • + *
* } * * @author Michael B. Klein @@ -58,12 +60,13 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceControlledVocabulary.class); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); protected static String xpathTemplate = "//node[contains(translate(@label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ'," + - "'abcdefghijklmnopqrstuvwxyz'),'%s')]"; - protected static String idTemplate = "//node[@id = '%s']"; - protected static String labelTemplate = "//node[@label = '%s']"; - protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy/parent::node"; + "'abcdefghijklmnopqrstuvwxyz'),%s)]"; + protected static String idTemplate = "//node[@id = %s]"; + protected static String idTemplateQuoted = "//node[@id = '%s']"; + protected static String labelTemplate = "//node[@label = %s]"; + protected static String idParentTemplate = "//node[@id = %s]/parent::isComposedBy/parent::node"; protected static String rootTemplate = "/node"; protected static String pluginNames[] = null; @@ -106,7 +109,7 @@ public boolean accept(File dir, String name) { File.separator + "config" + File.separator + "controlled-vocabularies"; String[] xmlFiles = (new File(vocabulariesPath)).list(new xmlFilter()); - List names = new ArrayList(); + List names = new ArrayList<>(); for (String filename : xmlFiles) { names.add((new File(filename)).getName().replace(".xml", "")); } @@ -177,8 +180,8 @@ public Choices getMatches(String text, int start, int limit, String locale) { } throw new IllegalArgumentException("Unexpected variable: " + varName); }); - int total = 0; - List choices = new ArrayList(); + int total; + List choices; try { NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); total = results.getLength(); @@ -194,7 +197,7 @@ public Choices getMatches(String text, int start, int limit, String locale) { @Override public Choices getBestMatch(String text, String locale) { init(); - log.debug("Getting best matches for '" + text + "'"); + log.debug("Getting best matches for {}'", text); String[] textHierarchy = text.split(hierarchyDelimiter, -1); StringBuilder xpathExpressionBuilder = new StringBuilder(); for (int i = 0; i < textHierarchy.length; i++) { @@ -210,7 +213,7 @@ public Choices getBestMatch(String text, String locale) { } throw new IllegalArgumentException("Unexpected variable: " + varName); }); - List choices = new ArrayList(); + List choices; try { NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); choices = getChoicesFromNodeList(results, 0, 1); @@ -282,15 +285,12 @@ public Integer getPreloadLevel() { } private boolean isRootElement(Node node) { - if (node != null && node.getOwnerDocument().getDocumentElement().equals(node)) { - return true; - } - return false; + return node != null && node.getOwnerDocument().getDocumentElement().equals(node); } private Node getNode(String key) throws XPathExpressionException { init(); - String xpathExpression = String.format(idTemplate, key); + String xpathExpression = String.format(idTemplateQuoted, key); Node node = getNodeFromXPath(xpathExpression); return node; } @@ -302,7 +302,7 @@ private Node getNodeFromXPath(String xpathExpression) throws XPathExpressionExce } private List getChoicesFromNodeList(NodeList results, int start, int limit) { - List choices = new ArrayList(); + List choices = new ArrayList<>(); for (int i = 0; i < results.getLength(); i++) { if (i < start) { continue; @@ -321,17 +321,17 @@ private List getChoicesFromNodeList(NodeList results, int start, int lim private Map addOtherInformation(String parentCurr, String noteCurr, List childrenCurr, String authorityCurr) { - Map extras = new HashMap(); + Map extras = new HashMap<>(); if (StringUtils.isNotBlank(parentCurr)) { extras.put("parent", parentCurr); } if (StringUtils.isNotBlank(noteCurr)) { extras.put("note", noteCurr); } - if (childrenCurr.size() > 0) { - extras.put("hasChildren", "true"); - } else { + if (childrenCurr.isEmpty()) { extras.put("hasChildren", "false"); + } else { + extras.put("hasChildren", "true"); } extras.put("id", authorityCurr); return extras; @@ -386,7 +386,7 @@ private String getNote(Node node) { } private List getChildren(Node node) { - List children = new ArrayList(); + List children = new ArrayList<>(); NodeList childNodes = node.getChildNodes(); for (int ci = 0; ci < childNodes.getLength(); ci++) { Node firstChild = childNodes.item(ci); @@ -409,7 +409,7 @@ private List getChildren(Node node) { private boolean isSelectable(Node node) { Node selectableAttr = node.getAttributes().getNamedItem("selectable"); if (null != selectableAttr) { - return Boolean.valueOf(selectableAttr.getNodeValue()); + return Boolean.parseBoolean(selectableAttr.getNodeValue()); } else { // Default is true return true; } @@ -436,7 +436,7 @@ private String getAuthority(Node node) { } private Choices getChoicesByXpath(String xpathExpression, int start, int limit) { - List choices = new ArrayList(); + List choices = new ArrayList<>(); XPath xpath = XPathFactory.newInstance().newXPath(); try { Node parentNode = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); From 0f6f9b5fb79512f70e3291c7bc2c1e1aca8d14a1 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 24 Jul 2025 14:01:57 -0400 Subject: [PATCH 221/701] Correct some assumptions about what should be quoted. --- .../dspace/content/authority/DSpaceControlledVocabulary.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 48d7c6999a96..1bef8b389898 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -66,7 +66,7 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements Hiera protected static String idTemplate = "//node[@id = %s]"; protected static String idTemplateQuoted = "//node[@id = '%s']"; protected static String labelTemplate = "//node[@label = %s]"; - protected static String idParentTemplate = "//node[@id = %s]/parent::isComposedBy/parent::node"; + protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy/parent::node"; protected static String rootTemplate = "/node"; protected static String pluginNames[] = null; @@ -261,7 +261,7 @@ public Choices getTopChoices(String authorityName, int start, int limit, String @Override public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) { init(); - String xpathExpression = String.format(idTemplate, parentId); + String xpathExpression = String.format(idTemplateQuoted, parentId); return getChoicesByXpath(xpathExpression, start, limit); } From e37a7ca74709565cfa5b117a9c8d667e349c12a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:41:53 +0000 Subject: [PATCH 222/701] Bump the build-tools group across 1 directory with 5 updates Bumps the build-tools group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [com.google.errorprone:error_prone_core](https://github.com/google/error-prone) | `2.38.0` | `2.39.0` | | [com.google.errorprone:error_prone_annotations](https://github.com/google/error-prone) | `2.38.0` | `2.39.0` | | [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) | `3.5.0` | `3.6.0` | | [org.apache.maven.plugins:maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) | `3.2.7` | `3.2.8` | | [org.codehaus.mojo:license-maven-plugin](https://github.com/mojohaus/license-maven-plugin) | `2.5.0` | `2.6.0` | Updates `com.google.errorprone:error_prone_core` from 2.38.0 to 2.39.0 - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.38.0...v2.39.0) Updates `com.google.errorprone:error_prone_annotations` from 2.38.0 to 2.39.0 - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.38.0...v2.39.0) Updates `com.google.errorprone:error_prone_annotations` from 2.38.0 to 2.39.0 - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.38.0...v2.39.0) Updates `org.apache.maven.plugins:maven-enforcer-plugin` from 3.5.0 to 3.6.0 - [Release notes](https://github.com/apache/maven-enforcer/releases) - [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.5.0...enforcer-3.6.0) Updates `org.apache.maven.plugins:maven-gpg-plugin` from 3.2.7 to 3.2.8 - [Release notes](https://github.com/apache/maven-gpg-plugin/releases) - [Commits](https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.2.7...maven-gpg-plugin-3.2.8) Updates `org.codehaus.mojo:license-maven-plugin` from 2.5.0 to 2.6.0 - [Release notes](https://github.com/mojohaus/license-maven-plugin/releases) - [Commits](https://github.com/mojohaus/license-maven-plugin/compare/2.5.0...2.6.0) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_core dependency-version: 2.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: com.google.errorprone:error_prone_annotations dependency-version: 2.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: com.google.errorprone:error_prone_annotations dependency-version: 2.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-enforcer-plugin dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-gpg-plugin dependency-version: 3.2.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.codehaus.mojo:license-maven-plugin dependency-version: 2.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index be7a9dde64d7..434037771140 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ 8.11.4 3.10.8 - 2.38.0 + 2.41.0 2.19.2 2.19.2 @@ -89,7 +89,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.5.0 + 3.6.1 enforce-java @@ -399,7 +399,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.7 + 3.2.8 @@ -693,7 +693,7 @@ org.codehaus.mojo license-maven-plugin - 2.5.0 + 2.6.0 false From af38697adee0ad4462b69805aec1ed9554e32bf0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 19:43:25 +0000 Subject: [PATCH 223/701] Bump the build-tools group across 1 directory with 2 updates Bumps the build-tools group with 2 updates in the / directory: [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) and [org.codehaus.mojo:license-maven-plugin](https://github.com/mojohaus/license-maven-plugin). Updates `org.apache.maven.plugins:maven-enforcer-plugin` from 3.5.0 to 3.6.0 - [Release notes](https://github.com/apache/maven-enforcer/releases) - [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.5.0...enforcer-3.6.0) Updates `org.codehaus.mojo:license-maven-plugin` from 2.5.0 to 2.6.0 - [Release notes](https://github.com/mojohaus/license-maven-plugin/releases) - [Commits](https://github.com/mojohaus/license-maven-plugin/compare/2.5.0...2.6.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-enforcer-plugin dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.codehaus.mojo:license-maven-plugin dependency-version: 2.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 78f1a388afed..9e1d52f0be60 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.5.0 + 3.6.1 enforce-java @@ -680,7 +680,7 @@ org.codehaus.mojo license-maven-plugin - 2.5.0 + 2.6.0 false From 9eeec6ce95a71bf6f6c81e0760b8baa55f52d1ce Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 28 Jul 2025 15:29:32 -0500 Subject: [PATCH 224/701] Fix errorprone discovered issues. Many tests are missing @Test annotation. Once enabled, a broken test was found & fixed in WorkflowItemRestRepositoryIT. --- .../org/dspace/app/rest/CollectionRestRepositoryIT.java | 1 + .../org/dspace/app/rest/WorkflowItemRestRepositoryIT.java | 6 ++++-- .../org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java | 3 +++ .../dspace/app/rest/authorization/RequestCopyFeatureIT.java | 5 +++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java index 8ddcbd6ad26a..7a2f049b4e1d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java @@ -1473,6 +1473,7 @@ public void updateCollectionEpersonWithWriteRightsTest() throws Exception { authorizeService.removePoliciesActionFilter(context, eperson, Constants.WRITE); } + @Test public void patchCollectionMetadataAuthorized() throws Exception { runPatchMetadataTests(admin, 200); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java index ef475353b93e..a16b20605be8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java @@ -913,6 +913,7 @@ public void unvalidCreateWorkflowItemTest() throws Exception { * * @throws Exception */ + @Test public void validationErrorsRequiredMetadataTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -936,6 +937,7 @@ public void validationErrorsRequiredMetadataTest() throws Exception { XmlWorkflowItem witem = WorkflowItemBuilder.createWorkflowItem(context, col1) .withTitle("Workflow Item 1") .withIssueDate("2017-10-17") + .grantLicense() .build(); //4. a workflow item without the dateissued required field @@ -947,12 +949,12 @@ public void validationErrorsRequiredMetadataTest() throws Exception { String authToken = getAuthToken(eperson.getEmail(), password); - getClient(authToken).perform(get("/api/workflow/worfklowitems/" + witem.getID())) + getClient(authToken).perform(get("/api/workflow/workflowitems/" + witem.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$.errors").doesNotExist()) ; - getClient(authToken).perform(get("/api/workflow/worfklowitems/" + witemMissingFields.getID())) + getClient(authToken).perform(get("/api/workflow/workflowitems/" + witemMissingFields.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$.errors[?(@.message=='error.validation.required')]", Matchers.contains( diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index 542688ea2396..24a389dcaf69 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -2618,6 +2618,7 @@ public void patchUpdateMetadataForbiddenTest() throws Exception { (witem, "Workspace Item 1", "2019-01-01", "ExtraEntry")))); } + @Test public void patchReplaceMetadataOnItemStillInSubmissionTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -5929,6 +5930,7 @@ public void patchBitstreamWithAccessConditionLeaseMissingDate() throws Exception .andExpect(jsonPath("$.sections.upload.files[0].accessConditions", empty())); } + @Test public void deleteWorkspaceItemWithMinRelationshipsTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -7983,6 +7985,7 @@ public void createItemWithoutEntityTypeIfCollectionHasBlankEntityType() throws E ))); } + @Test public void verifyBitstreamPolicyNotDuplicatedTest() throws Exception { context.turnOffAuthorisationSystem(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/RequestCopyFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/RequestCopyFeatureIT.java index 6fd5fad35c69..afae147515aa 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/RequestCopyFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/RequestCopyFeatureIT.java @@ -457,6 +457,7 @@ public void requestCopyOnBitstreamFromCollectionAsEperson() throws Exception { .andExpect(jsonPath("$._embedded").doesNotExist()); } + @Test public void requestACopyItemTypeLoggedAsAnonymous() throws Exception { configurationService.setProperty("request.item.type", "logged"); @@ -491,6 +492,7 @@ public void requestACopyItemTypeLoggedAsEperson() throws Exception { ); } + @Test public void requestACopyItemTypeEmptyAsAnonymous() throws Exception { configurationService.setProperty("request.item.type", ""); @@ -505,6 +507,7 @@ public void requestACopyItemTypeEmptyAsAnonymous() throws Exception { .andExpect(jsonPath("$._embedded").doesNotExist()); } + @Test public void requestACopyItemTypeEmptyAsEperson() throws Exception { configurationService.setProperty("request.item.type", ""); @@ -521,6 +524,7 @@ public void requestACopyItemTypeEmptyAsEperson() throws Exception { .andExpect(jsonPath("$._embedded").doesNotExist()); } + @Test public void requestACopyItemTypeBogusValueAsAnonymous() throws Exception { configurationService.setProperty("request.item.type", "invalid value"); @@ -535,6 +539,7 @@ public void requestACopyItemTypeBogusValueAsAnonymous() throws Exception { .andExpect(jsonPath("$._embedded").doesNotExist()); } + @Test public void requestACopyItemTypeBogusValueAsEperson() throws Exception { configurationService.setProperty("request.item.type", "invalid value"); From a4e1a0d57cce5d0263fe7deae04280a9696f954f Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 28 Jul 2025 15:31:09 -0500 Subject: [PATCH 225/701] Fix checkstyle indentation issue --- .../src/main/java/org/dspace/app/rest/LDNInboxController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java index f0ccbcf873c4..cff51520b3df 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java @@ -120,7 +120,7 @@ private void validate(Context context, Notification notification, String sourceI } } catch (SQLException sqle) { throw new DSpaceBadRequestException("Notify Service [" + notification.getOrigin() - + "] unknown. LDN message can not be received."); + + "] unknown. LDN message can not be received."); } } if (configurationService.getBooleanProperty("ldn.notify.inbox.block-untrusted-ip", true)) { @@ -138,7 +138,7 @@ private void validate(Context context, Notification notification, String sourceI } } catch (SQLException sqle) { throw new DSpaceBadRequestException("Notify Service [" + notification.getOrigin() - + "] unknown. LDN message can not be received."); + + "] unknown. LDN message can not be received."); } } } From f32ea48d20a77d49dcd72495db6010c464730f97 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 25 Jul 2025 11:13:02 -0500 Subject: [PATCH 226/701] Add tests for backend logging to docker deployment tests --- .github/workflows/docker.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9d32cb119d41..aea0bb6478ff 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -220,6 +220,19 @@ jobs: result=$(wget -O- -q http://127.0.0.1:8080/server/api/core/collections) echo "$result" echo "$result" | grep -oE "\"Dog in Yard\"," + # Verify basic backend logging is working. + # 1. Access the top communities list. Verify that the "Before request" INFO statement is logged + # 2. Access an invalid endpoint (and ignore 404 response). Verify that a "status:404" WARN statement is logged + - name: Verify backend is logging properly + run: | + wget -O/dev/null -q http://127.0.0.1:8080/server/api/core/communities/search/top + logs=$(docker compose -f docker-compose.yml logs -n 5 dspace) + echo "$logs" + echo "$logs" | grep -o "Before request \[GET /server/api/core/communities/search/top\]" + wget -O/dev/null -q http://127.0.0.1:8080/server/api/does/not/exist || true + logs=$(docker compose -f docker-compose.yml logs -n 5 dspace) + echo "$logs" + echo "$logs" | grep -o "status:404 exception: The repository type does.not was not found" # Verify Handle Server can be stared and is working properly # 1. First generate the "[dspace]/handle-server" folder with the sitebndl.zip # 2. Start the Handle Server (and wait 20 seconds to let it start up) From 5e3463bd8becba0ee105be00e9f5a117a12a02b3 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 25 Jul 2025 11:13:02 -0500 Subject: [PATCH 227/701] Add tests for backend logging to docker deployment tests --- .github/workflows/docker.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 815aec5cf6b4..079900745210 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -219,6 +219,19 @@ jobs: result=$(wget -O- -q http://127.0.0.1:8080/server/api/core/collections) echo "$result" echo "$result" | grep -oE "\"Dog in Yard\"," + # Verify basic backend logging is working. + # 1. Access the top communities list. Verify that the "Before request" INFO statement is logged + # 2. Access an invalid endpoint (and ignore 404 response). Verify that a "status:404" WARN statement is logged + - name: Verify backend is logging properly + run: | + wget -O/dev/null -q http://127.0.0.1:8080/server/api/core/communities/search/top + logs=$(docker compose -f docker-compose.yml logs -n 5 dspace) + echo "$logs" + echo "$logs" | grep -o "Before request \[GET /server/api/core/communities/search/top\]" + wget -O/dev/null -q http://127.0.0.1:8080/server/api/does/not/exist || true + logs=$(docker compose -f docker-compose.yml logs -n 5 dspace) + echo "$logs" + echo "$logs" | grep -o "status:404 exception: The repository type does.not was not found" # Verify Handle Server can be stared and is working properly # 1. First generate the "[dspace]/handle-server" folder with the sitebndl.zip # 2. Start the Handle Server (and wait 20 seconds to let it start up) From e50317499541945cb7eaecedc1d19405cca35060 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 22 Jul 2025 17:27:13 +0200 Subject: [PATCH 228/701] Add simple log4j WebappLoggingIT test (cherry picked from commit 588c4ef4d259bccb927440058639a00ace67fb11) --- .../dspace/app/rest/test/WebappLoggingIT.java | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java new file mode 100644 index 000000000000..fe746452c792 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/WebappLoggingIT.java @@ -0,0 +1,101 @@ +/** + * 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.app.rest.test; + +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.junit.After; +import org.junit.Test; + +/** + * Test basic log4j logging functionality, extending AbstractControllerIntegrationTest + * purely to make sure we are testing the *web application* and not just the kernel + * as that is where logging has broken in the past. + * + * @author Kim Shepherd + */ +public class WebappLoggingIT extends AbstractControllerIntegrationTest { + + private static final Logger logger = LogManager.getLogger(WebappLoggingIT.class); + private static final String APPENDER_NAME = "DSpaceTestAppender"; + + static class InMemoryAppender extends AbstractAppender { + private final List messages = new ArrayList<>(); + + protected InMemoryAppender(String name) { + super( + name, + null, + PatternLayout.newBuilder().withPattern("%m").build(), + false, + Property.EMPTY_ARRAY + ); + start(); + } + + @Override + public void append(LogEvent event) { + messages.add(event.getMessage().getFormattedMessage()); + } + + public List getMessages() { + return messages; + } + } + + @Test + public void testLogging() throws Exception { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration config = context.getConfiguration(); + + InMemoryAppender appender = new InMemoryAppender(APPENDER_NAME); + config.addAppender(appender); + + LoggerConfig testLoggerConfig = new LoggerConfig(logger.getName(), Level.INFO, false); + testLoggerConfig.addAppender(appender, null, null); + config.addLogger(logger.getName(), testLoggerConfig); + context.updateLoggers(); + + logger.info("DSPACE TEST LOG ENTRY"); + + List messages = appender.getMessages(); + assertTrue(messages.stream().anyMatch(msg -> msg.contains("DSPACE TEST LOG ENTRY"))); + } + + @After + public void cleanupAppender() { + LoggerContext context = (LoggerContext) LogManager.getContext(false); + Configuration config = context.getConfiguration(); + + config.removeLogger(logger.getName()); + + Appender appender = config.getAppender(APPENDER_NAME); + if (appender != null) { + appender.stop(); + config.getAppenders().remove(APPENDER_NAME); + } + + context.updateLoggers(); +} + +} + From c8ee72c0f27e5b8fa6e821f577ca574e7897ed3f Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 24 Feb 2025 09:16:19 -0600 Subject: [PATCH 229/701] Ensure DSpace defaults to UTC time zone in all code / tests. This is necessary so that Spring / Hibernate don't auto-switch timezones when reading from database. --- .../app/bulkaccesscontrol/BulkAccessControl.java | 3 ++- .../src/main/java/org/dspace/util/DateMathParser.java | 3 ++- .../org/dspace/AbstractDSpaceIntegrationTest.java | 7 +++++-- .../src/test/java/org/dspace/AbstractDSpaceTest.java | 7 +++++-- .../main/java/org/dspace/app/rest/WebApplication.java | 11 +++++++++++ 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java index 30f68efaf3cb..59e75059c94f 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java @@ -18,6 +18,7 @@ import java.sql.SQLException; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Date; import java.util.Iterator; @@ -154,7 +155,7 @@ public void internalRun() throws Exception { } ObjectMapper mapper = new ObjectMapper(); - mapper.setTimeZone(TimeZone.getTimeZone("UTC")); + mapper.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); BulkAccessControlInput accessControl; context = new Context(Context.Mode.BATCH_EDIT); setEPerson(context); diff --git a/dspace-api/src/main/java/org/dspace/util/DateMathParser.java b/dspace-api/src/main/java/org/dspace/util/DateMathParser.java index 9ff252e8ce3f..13f9216c9bdb 100644 --- a/dspace-api/src/main/java/org/dspace/util/DateMathParser.java +++ b/dspace-api/src/main/java/org/dspace/util/DateMathParser.java @@ -13,6 +13,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -107,7 +108,7 @@ public class DateMathParser { private static final Logger LOG = LogManager.getLogger(); - public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + public static final TimeZone UTC = TimeZone.getTimeZone(ZoneOffset.UTC); /** * Default TimeZone for DateMath rounding (UTC) diff --git a/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java b/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java index 791fdbc66abc..2822cdcf6018 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java +++ b/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.net.URL; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.Properties; import java.util.TimeZone; @@ -73,8 +74,10 @@ public static void initTestEnvironment() { //Stops System.exit(0) throws exception instead of exitting System.setSecurityManager(new NoExitSecurityManager()); - //set a standard time zone for the tests - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Dublin")); + // All tests should assume UTC timezone by default (unless overridden in the test itself) + // This ensures that Spring doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); //load the properties of the tests testProps = new Properties(); diff --git a/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java b/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java index 136af83f076f..4452955a3b64 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java +++ b/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.net.URL; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.Properties; import java.util.TimeZone; @@ -82,8 +83,10 @@ protected AbstractDSpaceTest() { } @BeforeClass public static void initKernel() { try { - //set a standard time zone for the tests - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Dublin")); + // All tests should assume UTC timezone by default (unless overridden in the test itself) + // This ensures that Spring doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); //load the properties of the tests testProps = new Properties(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java index 4c835d99183c..5494475b5241 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java @@ -9,8 +9,11 @@ import java.io.IOException; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.List; +import java.util.TimeZone; +import jakarta.annotation.PostConstruct; import jakarta.servlet.Filter; import org.dspace.app.ldn.LDNQueueExtractor; import org.dspace.app.ldn.LDNQueueTimeoutChecker; @@ -246,4 +249,12 @@ public void addArgumentResolvers(@NonNull List ar } }; } + + @PostConstruct + public void setDefaultTimezone() { + // Set the default timezone in Spring Boot to UTC. + // This ensures that Spring Boot doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); + } } From ca8de732348fd90e751169b261bf3e2a8a91d9ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 16:31:40 +0000 Subject: [PATCH 230/701] Bump the hibernate group with 3 updates Bumps the hibernate group with 3 updates: [org.hibernate.orm:hibernate-core](https://github.com/hibernate/hibernate-orm), [org.hibernate.orm:hibernate-jpamodelgen](https://github.com/hibernate/hibernate-orm) and [org.hibernate.orm:hibernate-jcache](https://github.com/hibernate/hibernate-orm). Updates `org.hibernate.orm:hibernate-core` from 6.4.8.Final to 6.4.10.Final - [Release notes](https://github.com/hibernate/hibernate-orm/releases) - [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.10/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.8...6.4.10) Updates `org.hibernate.orm:hibernate-jpamodelgen` from 6.4.8.Final to 6.4.10.Final - [Release notes](https://github.com/hibernate/hibernate-orm/releases) - [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.10/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.8...6.4.10) Updates `org.hibernate.orm:hibernate-jcache` from 6.4.8.Final to 6.4.10.Final - [Release notes](https://github.com/hibernate/hibernate-orm/releases) - [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.10/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.8...6.4.10) Updates `org.hibernate.orm:hibernate-jpamodelgen` from 6.4.8.Final to 6.4.10.Final - [Release notes](https://github.com/hibernate/hibernate-orm/releases) - [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.10/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.8...6.4.10) Updates `org.hibernate.orm:hibernate-jcache` from 6.4.8.Final to 6.4.10.Final - [Release notes](https://github.com/hibernate/hibernate-orm/releases) - [Changelog](https://github.com/hibernate/hibernate-orm/blob/6.4.10/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-orm/compare/6.4.8...6.4.10) --- updated-dependencies: - dependency-name: org.hibernate.orm:hibernate-core dependency-version: 6.4.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: hibernate - dependency-name: org.hibernate.orm:hibernate-jpamodelgen dependency-version: 6.4.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: hibernate - dependency-name: org.hibernate.orm:hibernate-jcache dependency-version: 6.4.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: hibernate - dependency-name: org.hibernate.orm:hibernate-jpamodelgen dependency-version: 6.4.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: hibernate - dependency-name: org.hibernate.orm:hibernate-jcache dependency-version: 6.4.10.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: hibernate ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 803d26cc9fa1..b6757304378e 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 6.2.9 3.5.4 6.5.2 - 6.4.8.Final + 6.4.10.Final 8.0.2.Final 42.7.7 10.22.0 From 2b2d0b19b68f45311d9774de8ace443fc4988d95 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 24 Feb 2025 09:16:19 -0600 Subject: [PATCH 231/701] Ensure DSpace defaults to UTC time zone in all code / tests. This is necessary so that Spring / Hibernate don't auto-switch timezones when reading from database. --- .../app/bulkaccesscontrol/BulkAccessControl.java | 3 ++- .../src/main/java/org/dspace/util/DateMathParser.java | 3 ++- .../org/dspace/AbstractDSpaceIntegrationTest.java | 7 +++++-- .../src/test/java/org/dspace/AbstractDSpaceTest.java | 7 +++++-- .../main/java/org/dspace/app/rest/Application.java | 11 +++++++++++ 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java index 30f68efaf3cb..59e75059c94f 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkaccesscontrol/BulkAccessControl.java @@ -18,6 +18,7 @@ import java.sql.SQLException; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Date; import java.util.Iterator; @@ -154,7 +155,7 @@ public void internalRun() throws Exception { } ObjectMapper mapper = new ObjectMapper(); - mapper.setTimeZone(TimeZone.getTimeZone("UTC")); + mapper.setTimeZone(TimeZone.getTimeZone(ZoneOffset.UTC)); BulkAccessControlInput accessControl; context = new Context(Context.Mode.BATCH_EDIT); setEPerson(context); diff --git a/dspace-api/src/main/java/org/dspace/util/DateMathParser.java b/dspace-api/src/main/java/org/dspace/util/DateMathParser.java index 9ff252e8ce3f..13f9216c9bdb 100644 --- a/dspace-api/src/main/java/org/dspace/util/DateMathParser.java +++ b/dspace-api/src/main/java/org/dspace/util/DateMathParser.java @@ -13,6 +13,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -107,7 +108,7 @@ public class DateMathParser { private static final Logger LOG = LogManager.getLogger(); - public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + public static final TimeZone UTC = TimeZone.getTimeZone(ZoneOffset.UTC); /** * Default TimeZone for DateMath rounding (UTC) diff --git a/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java b/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java index 791fdbc66abc..2822cdcf6018 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java +++ b/dspace-api/src/test/java/org/dspace/AbstractDSpaceIntegrationTest.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.net.URL; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.Properties; import java.util.TimeZone; @@ -73,8 +74,10 @@ public static void initTestEnvironment() { //Stops System.exit(0) throws exception instead of exitting System.setSecurityManager(new NoExitSecurityManager()); - //set a standard time zone for the tests - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Dublin")); + // All tests should assume UTC timezone by default (unless overridden in the test itself) + // This ensures that Spring doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); //load the properties of the tests testProps = new Properties(); diff --git a/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java b/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java index 136af83f076f..4452955a3b64 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java +++ b/dspace-api/src/test/java/org/dspace/AbstractDSpaceTest.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.net.URL; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.Properties; import java.util.TimeZone; @@ -82,8 +83,10 @@ protected AbstractDSpaceTest() { } @BeforeClass public static void initKernel() { try { - //set a standard time zone for the tests - TimeZone.setDefault(TimeZone.getTimeZone("Europe/Dublin")); + // All tests should assume UTC timezone by default (unless overridden in the test itself) + // This ensures that Spring doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); //load the properties of the tests testProps = new Properties(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java index 07b802b684ee..7dc878a5c7a3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java @@ -9,7 +9,10 @@ import java.io.IOException; import java.sql.SQLException; +import java.time.ZoneOffset; import java.util.List; +import java.util.TimeZone; +import javax.annotation.PostConstruct; import javax.servlet.Filter; import org.dspace.app.rest.filter.DSpaceRequestContextFilter; @@ -262,4 +265,12 @@ public void addArgumentResolvers(@NonNull List ar } }; } + + @PostConstruct + public void setDefaultTimezone() { + // Set the default timezone in Spring Boot to UTC. + // This ensures that Spring Boot doesn't attempt to change the timezone of dates that are read from the + // database (via Hibernate). We store all dates in the database as UTC. + TimeZone.setDefault(TimeZone.getTimeZone(ZoneOffset.UTC)); + } } From 367b95f6242e31bdf35ad2df5db15993ad3362b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:40:29 +0000 Subject: [PATCH 232/701] Bump the apache-commons group with 2 updates Bumps the apache-commons group with 2 updates: [org.apache.commons:commons-compress](https://github.com/apache/commons-compress) and [org.apache.commons:commons-csv](https://github.com/apache/commons-csv). Updates `org.apache.commons:commons-compress` from 1.27.1 to 1.28.0 - [Changelog](https://github.com/apache/commons-compress/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-compress/compare/rel/commons-compress-1.27.1...rel/commons-compress-1.28.0) Updates `org.apache.commons:commons-csv` from 1.14.0 to 1.14.1 - [Changelog](https://github.com/apache/commons-csv/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-csv/compare/rel/commons-csv-1.14.0...rel/commons-csv-1.14.1) --- updated-dependencies: - dependency-name: org.apache.commons:commons-compress dependency-version: 1.28.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-csv dependency-version: 1.14.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 803d26cc9fa1..74c2b00fea3d 100644 --- a/pom.xml +++ b/pom.xml @@ -1520,12 +1520,12 @@ org.apache.commons commons-compress - 1.27.1 + 1.28.0 org.apache.commons commons-csv - 1.14.0 + 1.14.1 org.apache.commons From 6282cb9f12101f552123e8644f4d4ec0dc0fd505 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:40:38 +0000 Subject: [PATCH 233/701] Bump org.apache.commons:commons-compress in the apache-commons group Bumps the apache-commons group with 1 update: [org.apache.commons:commons-compress](https://github.com/apache/commons-compress). Updates `org.apache.commons:commons-compress` from 1.27.1 to 1.28.0 - [Changelog](https://github.com/apache/commons-compress/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-compress/compare/rel/commons-compress-1.27.1...rel/commons-compress-1.28.0) --- updated-dependencies: - dependency-name: org.apache.commons:commons-compress dependency-version: 1.28.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2581f3b09ee8..14292ea0ff86 100644 --- a/pom.xml +++ b/pom.xml @@ -1515,7 +1515,7 @@ org.apache.commons commons-compress - 1.27.1 + 1.28.0 org.apache.commons From 6145b74dda63442a05428c15ef43b8e27c33cbc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:41:42 +0000 Subject: [PATCH 234/701] Bump com.github.spotbugs:spotbugs-maven-plugin from 4.9.3.0 to 4.9.3.2 Bumps [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) from 4.9.3.0 to 4.9.3.2. - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.3.0...spotbugs-maven-plugin-4.9.3.2) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 803d26cc9fa1..a5ae46d5b099 100644 --- a/pom.xml +++ b/pom.xml @@ -303,7 +303,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.3.0 + 4.9.3.2 Max Low From 1ecb5660f8b99e0eb0a54faa489132f40d6bbebf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:41:59 +0000 Subject: [PATCH 235/701] Bump com.github.spotbugs:spotbugs-maven-plugin from 4.9.3.0 to 4.9.3.2 Bumps [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) from 4.9.3.0 to 4.9.3.2. - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.3.0...spotbugs-maven-plugin-4.9.3.2) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2581f3b09ee8..b898ac20a7aa 100644 --- a/pom.xml +++ b/pom.xml @@ -295,7 +295,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.3.0 + 4.9.3.2 Max Low From 808135f688a8b56bf3e76260befe4361b53a60b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 18:42:52 +0000 Subject: [PATCH 236/701] Bump io.grpc:grpc-context from 1.73.0 to 1.74.0 Bumps [io.grpc:grpc-context](https://github.com/grpc/grpc-java) from 1.73.0 to 1.74.0. - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.73.0...v1.74.0) --- updated-dependencies: - dependency-name: io.grpc:grpc-context dependency-version: 1.74.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2581f3b09ee8..adc3809fa73a 100644 --- a/pom.xml +++ b/pom.xml @@ -1718,7 +1718,7 @@ io.grpc grpc-context - 1.73.0 + 1.74.0 com.google.http-client From 2d353ea995ff7e540dc1d81c96391abc53f6dc7e Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Thu, 13 Mar 2025 14:21:16 -0400 Subject: [PATCH 237/701] Fix checkpointing for bitstore migration Fixes the checkpointing for bitstore migration by actually committing the changes to the database. Replacing "dispatchEvents" with "commit" as testing has shown that this is necessary to ensure that the status of bitstreams is properly updated when the bitstore migration is interrupted. The provided integration tests fail if bitstreams successfully migrated before the interruption are not properly recorded in the database as being in the destination assetstore. (cherry picked from commit 3d7c45852d8b09fb15cddc1fb72c2a389e2b4d6a) --- .../bitstore/BitstreamStorageServiceImpl.java | 2 +- .../BitstreamStorageServiceImplIT.java | 262 ++++++++++++++++++ 2 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java index 956ac5a7f8f1..dcdc238298fc 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java @@ -423,7 +423,7 @@ public void migrate(Context context, Integer assetstoreSource, Integer assetstor //modulo if ((processedCounter % batchCommitSize) == 0) { log.info("Migration Commit Checkpoint: " + processedCounter); - context.dispatchEvents(); + context.commit(); } } diff --git a/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java b/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java new file mode 100644 index 000000000000..5b15cba1c4c1 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java @@ -0,0 +1,262 @@ +/** + * 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.storage.bitstore; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; +import org.dspace.core.Context; +import org.dspace.storage.bitstore.factory.StorageServiceFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class BitstreamStorageServiceImplIT extends AbstractIntegrationTestWithDatabase { + private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + private BitstreamStorageServiceImpl bitstreamStorageService = + (BitstreamStorageServiceImpl) StorageServiceFactory.getInstance().getBitstreamStorageService(); + private Collection collection; + + private Map originalBitstores; + + private static final Integer SOURCE_STORE = 0; + private static final Integer DEST_STORE = 1; + + @Rule + public final TemporaryFolder tempStoreDir = new TemporaryFolder(); + + @Before + public void setup() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .build(); + + collection = CollectionBuilder.createCollection(context, parentCommunity) + .build(); + + originalBitstores = bitstreamStorageService.getStores(); + Map stores = new HashMap<>(); + DSBitStoreService sourceStore = new DSBitStoreService(); + sourceStore.setBaseDir(tempStoreDir.newFolder("src")); + + stores.put(SOURCE_STORE, sourceStore); + bitstreamStorageService.setStores(stores); + + context.restoreAuthSystemState(); + } + + @After + public void cleanUp() throws IOException { + // Restore the bitstore storage stores + bitstreamStorageService.setStores(originalBitstores); + } + + /** + * Test batch commit checkpointing, using the default batch commit size of 1 + * + * @throws Exception if an exception occurs. + */ + @Test + public void testDefaultBatchCommitSize() throws Exception { + Context context = this.context; + + // Destination assetstore fails after two bitstreams have been migrated + DSBitStoreService destinationStore = new LimitedTempDSBitStoreService(tempStoreDir, 2); + Map stores = bitstreamStorageService.getStores(); + stores.put(DEST_STORE, destinationStore); + + // Create three bitstreams in the source assetstore + createBitstreams(context, 3); + + // Three bitstreams in source assetstore at the start + assertThat(bitstreamService.countByStoreNumber(context, SOURCE_STORE).intValue(), equalTo(3)); + + // No bitstreams in destination assetstore at the start + assertThat(bitstreamService.countByStoreNumber(context, DEST_STORE).intValue(), equalTo(0)); + + /// Commit any pending transaction to database + context.commit(); + + // Migrate bitstreams + context.turnOffAuthorisationSystem(); + + boolean deleteOld = false; + Integer batchCommitSize = 1; + try { + bitstreamStorageService.migrate( + context, SOURCE_STORE, DEST_STORE, deleteOld, + batchCommitSize + ); + fail("IOException should have been thrown"); + } catch (IOException ioe) { + // Rollback any pending transaction + context.rollback(); + } + + context.restoreAuthSystemState(); + + // One bitstream should still be in the source assetstore, due to the + // interrupted migration + assertThat(bitstreamService.countByStoreNumber(context, SOURCE_STORE).intValue(), equalTo(1)); + + // Two bitstreams should have migrated to the destination assetstore + assertThat(bitstreamService.countByStoreNumber(context, DEST_STORE).intValue(), equalTo(2)); + } + + /** + * Test batch commit checkpointing, using the default batch commit size of 3 + * + * @throws Exception if an exception occurs. + */ + @Test + public void testBatchCommitSizeThree() throws Exception { + Context context = this.context; + + // Destination assetstore fails after four bitstreams have been migrated + DSBitStoreService destinationStore = new LimitedTempDSBitStoreService(tempStoreDir, 4); + Map stores = bitstreamStorageService.getStores(); + stores.put(DEST_STORE, destinationStore); + + // Create five bitstreams in the source assetstore + createBitstreams(context, 5); + + // Five bitstreams in source assetstore at the start + assertThat(bitstreamService.countByStoreNumber(context, SOURCE_STORE).intValue(), equalTo(5)); + + // No bitstreams in destination assetstore at the start + assertThat(bitstreamService.countByStoreNumber(context, DEST_STORE).intValue(), equalTo(0)); + + // Commit any pending transaction to database + context.commit(); + + // Migrate bitstreams + context.turnOffAuthorisationSystem(); + + boolean deleteOld = false; + Integer batchCommitSize = 3; + try { + bitstreamStorageService.migrate( + context, SOURCE_STORE, DEST_STORE, deleteOld, + batchCommitSize + ); + fail("IOException should have been thrown"); + } catch (IOException ioe) { + // Rollback any pending transaction + context.rollback(); + } + + context.restoreAuthSystemState(); + + // Since the batch commit size is 3, only three bitstreams should be + // marked as migrated, so there should still be two bitstreams + // in the source assetstore, due to the interrupted migration + assertThat(bitstreamService.countByStoreNumber(context, SOURCE_STORE).intValue(), equalTo(2)); + + // Three bitstreams should have migrated to the destination assetstore + assertThat(bitstreamService.countByStoreNumber(context, DEST_STORE).intValue(), equalTo(3)); + } + + private void createBitstreams(Context context, int numBitstreams) + throws SQLException { + context.turnOffAuthorisationSystem(); + for (int i = 0; i < numBitstreams; i++) { + String content = "Test bitstream " + i; + createBitstream(content); + } + context.restoreAuthSystemState(); + context.commit(); + } + + private Bitstream createBitstream(String content) { + try { + return BitstreamBuilder + .createBitstream(context, createItem(), toInputStream(content)) + .build(); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(e); + } + } + + private Item createItem() { + return ItemBuilder.createItem(context, collection) + .withTitle("Test item") + .build(); + } + + + private InputStream toInputStream(String content) { + return IOUtils.toInputStream(content, UTF_8); + } + + + /** + * DSBitStoreService variation that only allows a limited number of puts + * to the bit store before throwing an IOException, to test the + * error handling of the BitstreamStorageService.migrate() method. + */ + class LimitedTempDSBitStoreService extends DSBitStoreService { + // The number of put calls allowed before throwing an IOException + protected int maxPuts = Integer.MAX_VALUE; + + // The number of "put" method class seen so far. + protected int putCallCount = 0; + + /** + * Constructor. + * + * @param maxPuts the number of put calls to allow before throwing an + * IOException + */ + public LimitedTempDSBitStoreService(TemporaryFolder tempStoreDir, int maxPuts) throws IOException { + super(); + setBaseDir(tempStoreDir.newFolder()); + this.maxPuts = maxPuts; + } + + /** + * Store a stream of bits. + * + * After "maxPut" number of calls, this method throws an IOException. + * @param in The stream of bits to store + * @throws java.io.IOException If a problem occurs while storing the bits + */ + @Override + public void put(Bitstream bitstream, InputStream in) throws IOException { + putCallCount = putCallCount + 1; + if (putCallCount > maxPuts) { + throw new IOException("Max 'put' method calls exceeded"); + } else { + super.put(bitstream, in); + } + } + } +} From 79b39e2306e1fa02a918b9bc4de34a0d3001bced Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Thu, 13 Mar 2025 14:21:16 -0400 Subject: [PATCH 238/701] Fix checkpointing for bitstore migration Fixes the checkpointing for bitstore migration by actually committing the changes to the database. Replacing "dispatchEvents" with "commit" as testing has shown that this is necessary to ensure that the status of bitstreams is properly updated when the bitstore migration is interrupted. The provided integration tests fail if bitstreams successfully migrated before the interruption are not properly recorded in the database as being in the destination assetstore. (cherry picked from commit 3d7c45852d8b09fb15cddc1fb72c2a389e2b4d6a) --- .../bitstore/BitstreamStorageServiceImpl.java | 2 +- .../BitstreamStorageServiceImplIT.java | 262 ++++++++++++++++++ 2 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java index c89e5d7a54d1..9ebf3b29849c 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java @@ -423,7 +423,7 @@ public void migrate(Context context, Integer assetstoreSource, Integer assetstor //modulo if ((processedCounter % batchCommitSize) == 0) { log.info("Migration Commit Checkpoint: " + processedCounter); - context.dispatchEvents(); + context.commit(); } } diff --git a/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java b/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java new file mode 100644 index 000000000000..5b15cba1c4c1 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/storage/bitstore/BitstreamStorageServiceImplIT.java @@ -0,0 +1,262 @@ +/** + * 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.storage.bitstore; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; +import org.dspace.core.Context; +import org.dspace.storage.bitstore.factory.StorageServiceFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class BitstreamStorageServiceImplIT extends AbstractIntegrationTestWithDatabase { + private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + private BitstreamStorageServiceImpl bitstreamStorageService = + (BitstreamStorageServiceImpl) StorageServiceFactory.getInstance().getBitstreamStorageService(); + private Collection collection; + + private Map originalBitstores; + + private static final Integer SOURCE_STORE = 0; + private static final Integer DEST_STORE = 1; + + @Rule + public final TemporaryFolder tempStoreDir = new TemporaryFolder(); + + @Before + public void setup() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .build(); + + collection = CollectionBuilder.createCollection(context, parentCommunity) + .build(); + + originalBitstores = bitstreamStorageService.getStores(); + Map stores = new HashMap<>(); + DSBitStoreService sourceStore = new DSBitStoreService(); + sourceStore.setBaseDir(tempStoreDir.newFolder("src")); + + stores.put(SOURCE_STORE, sourceStore); + bitstreamStorageService.setStores(stores); + + context.restoreAuthSystemState(); + } + + @After + public void cleanUp() throws IOException { + // Restore the bitstore storage stores + bitstreamStorageService.setStores(originalBitstores); + } + + /** + * Test batch commit checkpointing, using the default batch commit size of 1 + * + * @throws Exception if an exception occurs. + */ + @Test + public void testDefaultBatchCommitSize() throws Exception { + Context context = this.context; + + // Destination assetstore fails after two bitstreams have been migrated + DSBitStoreService destinationStore = new LimitedTempDSBitStoreService(tempStoreDir, 2); + Map stores = bitstreamStorageService.getStores(); + stores.put(DEST_STORE, destinationStore); + + // Create three bitstreams in the source assetstore + createBitstreams(context, 3); + + // Three bitstreams in source assetstore at the start + assertThat(bitstreamService.countByStoreNumber(context, SOURCE_STORE).intValue(), equalTo(3)); + + // No bitstreams in destination assetstore at the start + assertThat(bitstreamService.countByStoreNumber(context, DEST_STORE).intValue(), equalTo(0)); + + /// Commit any pending transaction to database + context.commit(); + + // Migrate bitstreams + context.turnOffAuthorisationSystem(); + + boolean deleteOld = false; + Integer batchCommitSize = 1; + try { + bitstreamStorageService.migrate( + context, SOURCE_STORE, DEST_STORE, deleteOld, + batchCommitSize + ); + fail("IOException should have been thrown"); + } catch (IOException ioe) { + // Rollback any pending transaction + context.rollback(); + } + + context.restoreAuthSystemState(); + + // One bitstream should still be in the source assetstore, due to the + // interrupted migration + assertThat(bitstreamService.countByStoreNumber(context, SOURCE_STORE).intValue(), equalTo(1)); + + // Two bitstreams should have migrated to the destination assetstore + assertThat(bitstreamService.countByStoreNumber(context, DEST_STORE).intValue(), equalTo(2)); + } + + /** + * Test batch commit checkpointing, using the default batch commit size of 3 + * + * @throws Exception if an exception occurs. + */ + @Test + public void testBatchCommitSizeThree() throws Exception { + Context context = this.context; + + // Destination assetstore fails after four bitstreams have been migrated + DSBitStoreService destinationStore = new LimitedTempDSBitStoreService(tempStoreDir, 4); + Map stores = bitstreamStorageService.getStores(); + stores.put(DEST_STORE, destinationStore); + + // Create five bitstreams in the source assetstore + createBitstreams(context, 5); + + // Five bitstreams in source assetstore at the start + assertThat(bitstreamService.countByStoreNumber(context, SOURCE_STORE).intValue(), equalTo(5)); + + // No bitstreams in destination assetstore at the start + assertThat(bitstreamService.countByStoreNumber(context, DEST_STORE).intValue(), equalTo(0)); + + // Commit any pending transaction to database + context.commit(); + + // Migrate bitstreams + context.turnOffAuthorisationSystem(); + + boolean deleteOld = false; + Integer batchCommitSize = 3; + try { + bitstreamStorageService.migrate( + context, SOURCE_STORE, DEST_STORE, deleteOld, + batchCommitSize + ); + fail("IOException should have been thrown"); + } catch (IOException ioe) { + // Rollback any pending transaction + context.rollback(); + } + + context.restoreAuthSystemState(); + + // Since the batch commit size is 3, only three bitstreams should be + // marked as migrated, so there should still be two bitstreams + // in the source assetstore, due to the interrupted migration + assertThat(bitstreamService.countByStoreNumber(context, SOURCE_STORE).intValue(), equalTo(2)); + + // Three bitstreams should have migrated to the destination assetstore + assertThat(bitstreamService.countByStoreNumber(context, DEST_STORE).intValue(), equalTo(3)); + } + + private void createBitstreams(Context context, int numBitstreams) + throws SQLException { + context.turnOffAuthorisationSystem(); + for (int i = 0; i < numBitstreams; i++) { + String content = "Test bitstream " + i; + createBitstream(content); + } + context.restoreAuthSystemState(); + context.commit(); + } + + private Bitstream createBitstream(String content) { + try { + return BitstreamBuilder + .createBitstream(context, createItem(), toInputStream(content)) + .build(); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(e); + } + } + + private Item createItem() { + return ItemBuilder.createItem(context, collection) + .withTitle("Test item") + .build(); + } + + + private InputStream toInputStream(String content) { + return IOUtils.toInputStream(content, UTF_8); + } + + + /** + * DSBitStoreService variation that only allows a limited number of puts + * to the bit store before throwing an IOException, to test the + * error handling of the BitstreamStorageService.migrate() method. + */ + class LimitedTempDSBitStoreService extends DSBitStoreService { + // The number of put calls allowed before throwing an IOException + protected int maxPuts = Integer.MAX_VALUE; + + // The number of "put" method class seen so far. + protected int putCallCount = 0; + + /** + * Constructor. + * + * @param maxPuts the number of put calls to allow before throwing an + * IOException + */ + public LimitedTempDSBitStoreService(TemporaryFolder tempStoreDir, int maxPuts) throws IOException { + super(); + setBaseDir(tempStoreDir.newFolder()); + this.maxPuts = maxPuts; + } + + /** + * Store a stream of bits. + * + * After "maxPut" number of calls, this method throws an IOException. + * @param in The stream of bits to store + * @throws java.io.IOException If a problem occurs while storing the bits + */ + @Override + public void put(Bitstream bitstream, InputStream in) throws IOException { + putCallCount = putCallCount + 1; + if (putCallCount > maxPuts) { + throw new IOException("Max 'put' method calls exceeded"); + } else { + super.put(bitstream, in); + } + } + } +} From a9ff57dddc00d64cbd169cb1c1d570df296ee927 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 26 May 2025 10:15:40 +0200 Subject: [PATCH 239/701] [TLC-1117] Inherit custom, non-admin policies when creating new bundles Also shifted some resource policy methods from ItemService to AuthorizeService as they seemed better suited there. (cherry picked from commit 924678a092775124f56cc114126ea52a163550fa) --- .../authorize/AuthorizeServiceImpl.java | 117 +++++++++++++++++- .../authorize/service/AuthorizeService.java | 19 +++ .../org/dspace/content/ItemServiceImpl.java | 96 +------------- .../authorize/AuthorizeServiceTest.java | 97 +++++++++++++++ 4 files changed, 235 insertions(+), 94 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java index f2692cf394fc..60004bda9538 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.UUID; import org.apache.commons.collections4.CollectionUtils; @@ -48,6 +49,7 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; import org.dspace.workflow.WorkflowItemService; import org.springframework.beans.factory.annotation.Autowired; @@ -84,6 +86,8 @@ public class AuthorizeServiceImpl implements AuthorizeService { protected WorkflowItemService workflowItemService; @Autowired(required = true) private SearchService searchService; + @Autowired(required = true) + private ConfigurationService configurationService; protected AuthorizeServiceImpl() { @@ -508,17 +512,26 @@ public List getPoliciesActionFilter(Context c, DSpaceObject o, return resourcePolicyService.find(c, o, actionID); } + @Override + public void inheritPolicies(Context c, DSpaceObject src, DSpaceObject dest) + throws SQLException, AuthorizeException { + inheritPolicies(c, src, dest, false); + } + @Override public void inheritPolicies(Context c, DSpaceObject src, - DSpaceObject dest) throws SQLException, AuthorizeException { + DSpaceObject dest, boolean includeCustom) throws SQLException, AuthorizeException { // find all policies for the source object List policies = getPolicies(c, src); - //Only inherit non-ADMIN policies (since ADMIN policies are automatically inherited) - //and non-custom policies as these are manually applied when appropriate + // Only inherit non-ADMIN policies (since ADMIN policies are automatically inherited) + // and non-custom policies (usually applied manually?) UNLESS specified otherwise with includCustom + // (for example, item.addBundle() will inherit custom policies to enforce access conditions) List nonAdminPolicies = new ArrayList<>(); for (ResourcePolicy rp : policies) { - if (rp.getAction() != Constants.ADMIN && !StringUtils.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM)) { + if (rp.getAction() != Constants.ADMIN && (!StringUtils.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM) + || (includeCustom && StringUtils.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM) + && isNotAlreadyACustomRPOfThisTypeOnDSO(c, dest)))) { nonAdminPolicies.add(rp); } } @@ -943,4 +956,100 @@ private String formatCustomQuery(String query) { return query + " AND "; } } + + /** + * Add the default policies, which have not been already added to the given DSpace object + * + * @param context The relevant DSpace Context. + * @param dso The DSpace Object to add policies to + * @param defaultCollectionPolicies list of policies + * @throws SQLException An exception that provides information on a database access error or other errors. + * @throws AuthorizeException Exception indicating the current user of the context does not have permission + * to perform a particular action. + */ + @Override + public void addDefaultPoliciesNotInPlace(Context context, DSpaceObject dso, + List defaultCollectionPolicies) throws SQLException, AuthorizeException { + boolean appendMode = configurationService + .getBooleanProperty("core.authorization.installitem.inheritance-read.append-mode", false); + for (ResourcePolicy defaultPolicy : defaultCollectionPolicies) { + if (!isAnIdenticalPolicyAlreadyInPlace(context, dso, defaultPolicy.getGroup(), Constants.READ, + defaultPolicy.getID()) && + (!appendMode && isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) || + appendMode && shouldBeAppended(context, dso, defaultPolicy))) { + ResourcePolicy newPolicy = resourcePolicyService.clone(context, defaultPolicy); + newPolicy.setdSpaceObject(dso); + newPolicy.setAction(Constants.READ); + newPolicy.setRpType(ResourcePolicy.TYPE_INHERITED); + resourcePolicyService.update(context, newPolicy); + } + } + } + + /** + * Add a list of custom policies if there are already NO custom policies in place + * + */ + @Override + public void addCustomPoliciesNotInPlace(Context context, DSpaceObject dso, List customPolicies) + throws SQLException, AuthorizeException { + boolean customPoliciesAlreadyInPlace = + findPoliciesByDSOAndType(context, dso, ResourcePolicy.TYPE_CUSTOM).size() > 0; + if (!customPoliciesAlreadyInPlace) { + addPolicies(context, customPolicies, dso); + } + } + + /** + * Check whether or not there is already an RP on the given dso, which has actionId={@link Constants.READ} and + * resourceTypeId={@link ResourcePolicy.TYPE_CUSTOM} + * + * @param context DSpace context + * @param dso DSpace object to check for custom read RP + * @return True if there is no RP on the item with custom read RP, otherwise false + * @throws SQLException If something goes wrong retrieving the RP on the DSO + */ + private boolean isNotAlreadyACustomRPOfThisTypeOnDSO(Context context, DSpaceObject dso) throws SQLException { + return isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso, Constants.READ); + } + + private boolean isNotAlreadyACustomRPOfThisTypeOnDSO(Context context, DSpaceObject dso, int action) + throws SQLException { + List rps = resourcePolicyService.find(context, dso, action); + for (ResourcePolicy rp : rps) { + if (rp.getRpType() != null && rp.getRpType().equals(ResourcePolicy.TYPE_CUSTOM)) { + return false; + } + } + return true; + } + + /** + * Check if the provided default policy should be appended or not to the final + * item. If an item has at least one custom READ policy any anonymous READ + * policy with empty start/end date should be skipped + * + * @param context DSpace context + * @param dso DSpace object to check for custom read RP + * @param defaultPolicy The policy to check + * @return + * @throws SQLException If something goes wrong retrieving the RP on the DSO + */ + private boolean shouldBeAppended(Context context, DSpaceObject dso, ResourcePolicy defaultPolicy) + throws SQLException { + boolean hasCustomPolicy = resourcePolicyService.find(context, dso, Constants.READ) + .stream() + .filter(rp -> (Objects.nonNull(rp.getRpType()) && + Objects.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM))) + .findFirst() + .isPresent(); + + boolean isAnonymousGroup = Objects.nonNull(defaultPolicy.getGroup()) + && StringUtils.equals(defaultPolicy.getGroup().getName(), Group.ANONYMOUS); + + boolean datesAreNull = Objects.isNull(defaultPolicy.getStartDate()) + && Objects.isNull(defaultPolicy.getEndDate()); + + return !(hasCustomPolicy && isAnonymousGroup && datesAreNull); + } } diff --git a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java index e0a94833d76c..0e542d98f6d4 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java +++ b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java @@ -322,6 +322,19 @@ public void addPolicy(Context c, DSpaceObject o, int actionID, Group g, String t */ public List getPoliciesActionFilterExceptRpType(Context c, DSpaceObject o, int actionID, String rpType) throws SQLException; + /** + * Add policies to an object to match those from a previous object + * + * @param c context + * @param src source of policies + * @param dest destination of inherited policies + * @param includeCustom whether TYPE_CUSTOM policies should be inherited + * @throws SQLException if there's a database problem + * @throws AuthorizeException if the current user is not authorized to add these policies + */ + public void inheritPolicies(Context c, DSpaceObject src, DSpaceObject dest, boolean includeCustom) + throws SQLException, AuthorizeException; + /** * Add policies to an object to match those from a previous object * @@ -604,4 +617,10 @@ long countAdminAuthorizedCollection(Context context, String query) public void replaceAllPolicies(Context context, DSpaceObject source, DSpaceObject dest) throws SQLException, AuthorizeException; + public void addDefaultPoliciesNotInPlace(Context context, DSpaceObject dso, + List defaultCollectionPolicies) throws SQLException, AuthorizeException; + + public void addCustomPoliciesNotInPlace(Context context, DSpaceObject dso, + List defaultCollectionPolicies) throws SQLException, AuthorizeException; + } diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 157b891486f0..99dc9459d84f 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -480,7 +480,7 @@ public void addBundle(Context context, Item item, Bundle bundle) throws SQLExcep // now add authorization policies from owning item // hmm, not very "multiple-inclusion" friendly - authorizeService.inheritPolicies(context, item, bundle); + authorizeService.inheritPolicies(context, item, bundle, true); // Add the bundle to in-memory list item.addBundle(bundle); @@ -1046,8 +1046,8 @@ public void adjustBundleBitstreamPolicies(Context context, Item item, Collection // if come from InstallItem: remove all submission/workflow policies authorizeService.removeAllPoliciesByDSOAndType(context, mybundle, ResourcePolicy.TYPE_SUBMISSION); authorizeService.removeAllPoliciesByDSOAndType(context, mybundle, ResourcePolicy.TYPE_WORKFLOW); - addCustomPoliciesNotInPlace(context, mybundle, defaultItemPolicies); - addDefaultPoliciesNotInPlace(context, mybundle, defaultCollectionBundlePolicies); + authorizeService.addCustomPoliciesNotInPlace(context, mybundle, defaultItemPolicies); + authorizeService.addDefaultPoliciesNotInPlace(context, mybundle, defaultCollectionBundlePolicies); for (Bitstream bitstream : mybundle.getBitstreams()) { // If collection has default READ policies, remove the bundle's READ policies. @@ -1093,8 +1093,8 @@ private void removeAllPoliciesAndAddDefault(Context context, Bitstream bitstream throws SQLException, AuthorizeException { authorizeService.removeAllPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_SUBMISSION); authorizeService.removeAllPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_WORKFLOW); - addCustomPoliciesNotInPlace(context, bitstream, defaultItemPolicies); - addDefaultPoliciesNotInPlace(context, bitstream, defaultCollectionPolicies); + authorizeService.addCustomPoliciesNotInPlace(context, bitstream, defaultItemPolicies); + authorizeService.addDefaultPoliciesNotInPlace(context, bitstream, defaultCollectionPolicies); } @Override @@ -1132,7 +1132,7 @@ public void adjustItemPolicies(Context context, Item item, Collection collection authorizeService.removeAllPoliciesByDSOAndType(context, item, ResourcePolicy.TYPE_WORKFLOW); // add default policies only if not already in place - addDefaultPoliciesNotInPlace(context, item, defaultCollectionPolicies); + authorizeService.addDefaultPoliciesNotInPlace(context, item, defaultCollectionPolicies); } finally { context.restoreAuthSystemState(); } @@ -1322,91 +1322,7 @@ public boolean isInProgressSubmission(Context context, Item item) throws SQLExce */ - /** - * Add the default policies, which have not been already added to the given DSpace object - * - * @param context The relevant DSpace Context. - * @param dso The DSpace Object to add policies to - * @param defaultCollectionPolicies list of policies - * @throws SQLException An exception that provides information on a database access error or other errors. - * @throws AuthorizeException Exception indicating the current user of the context does not have permission - * to perform a particular action. - */ - protected void addDefaultPoliciesNotInPlace(Context context, DSpaceObject dso, - List defaultCollectionPolicies) throws SQLException, AuthorizeException { - boolean appendMode = configurationService - .getBooleanProperty("core.authorization.installitem.inheritance-read.append-mode", false); - for (ResourcePolicy defaultPolicy : defaultCollectionPolicies) { - if (!authorizeService - .isAnIdenticalPolicyAlreadyInPlace(context, dso, defaultPolicy.getGroup(), Constants.READ, - defaultPolicy.getID()) && - (!appendMode && isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) || - appendMode && shouldBeAppended(context, dso, defaultPolicy))) { - ResourcePolicy newPolicy = resourcePolicyService.clone(context, defaultPolicy); - newPolicy.setdSpaceObject(dso); - newPolicy.setAction(Constants.READ); - newPolicy.setRpType(ResourcePolicy.TYPE_INHERITED); - resourcePolicyService.update(context, newPolicy); - } - } - } - private void addCustomPoliciesNotInPlace(Context context, DSpaceObject dso, List customPolicies) - throws SQLException, AuthorizeException { - boolean customPoliciesAlreadyInPlace = authorizeService - .findPoliciesByDSOAndType(context, dso, ResourcePolicy.TYPE_CUSTOM).size() > 0; - if (!customPoliciesAlreadyInPlace) { - authorizeService.addPolicies(context, customPolicies, dso); - } - } - - /** - * Check whether or not there is already an RP on the given dso, which has actionId={@link Constants.READ} and - * resourceTypeId={@link ResourcePolicy.TYPE_CUSTOM} - * - * @param context DSpace context - * @param dso DSpace object to check for custom read RP - * @return True if there is no RP on the item with custom read RP, otherwise false - * @throws SQLException If something goes wrong retrieving the RP on the DSO - */ - private boolean isNotAlreadyACustomRPOfThisTypeOnDSO(Context context, DSpaceObject dso) throws SQLException { - List readRPs = resourcePolicyService.find(context, dso, Constants.READ); - for (ResourcePolicy readRP : readRPs) { - if (readRP.getRpType() != null && readRP.getRpType().equals(ResourcePolicy.TYPE_CUSTOM)) { - return false; - } - } - return true; - } - - /** - * Check if the provided default policy should be appended or not to the final - * item. If an item has at least one custom READ policy any anonymous READ - * policy with empty start/end date should be skipped - * - * @param context DSpace context - * @param dso DSpace object to check for custom read RP - * @param defaultPolicy The policy to check - * @return - * @throws SQLException If something goes wrong retrieving the RP on the DSO - */ - private boolean shouldBeAppended(Context context, DSpaceObject dso, ResourcePolicy defaultPolicy) - throws SQLException { - boolean hasCustomPolicy = resourcePolicyService.find(context, dso, Constants.READ) - .stream() - .filter(rp -> (Objects.nonNull(rp.getRpType()) && - Objects.equals(rp.getRpType(), ResourcePolicy.TYPE_CUSTOM))) - .findFirst() - .isPresent(); - - boolean isAnonimousGroup = Objects.nonNull(defaultPolicy.getGroup()) - && StringUtils.equals(defaultPolicy.getGroup().getName(), Group.ANONYMOUS); - - boolean datesAreNull = Objects.isNull(defaultPolicy.getStartDate()) - && Objects.isNull(defaultPolicy.getEndDate()); - - return !(hasCustomPolicy && isAnonimousGroup && datesAreNull); - } /** * Returns an iterator of Items possessing the passed metadata field, or only diff --git a/dspace-api/src/test/java/org/dspace/authorize/AuthorizeServiceTest.java b/dspace-api/src/test/java/org/dspace/authorize/AuthorizeServiceTest.java index 70eaa2a0b909..e8bb428db53a 100644 --- a/dspace-api/src/test/java/org/dspace/authorize/AuthorizeServiceTest.java +++ b/dspace-api/src/test/java/org/dspace/authorize/AuthorizeServiceTest.java @@ -9,14 +9,24 @@ package org.dspace.authorize; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; import org.dspace.AbstractUnitTest; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BundleService; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; +import org.dspace.content.service.InstallItemService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Constants; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; @@ -38,6 +48,10 @@ public class AuthorizeServiceTest extends AbstractUnitTest { .getResourcePolicyService(); protected CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); protected CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService(); + protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + protected BundleService bundleService = ContentServiceFactory.getInstance().getBundleService(); + protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); + protected InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); public AuthorizeServiceTest() { } @@ -127,6 +141,89 @@ public void testauthorizeMethodRespectSpecialGroups() { throw new AssertionError(ex); } } + + /** + * When a bundle is created it should inherit custom policies (deduped) + * from the item, as otherwise bitstream bundles created via filter-media etc. + * will be created without READ policies + */ + @Test + public void testInheritanceOfCustomPolicies() { + try { + context.turnOffAuthorisationSystem(); + Community community = communityService.create(null, context); + Collection collection = collectionService.create(context, community); + WorkspaceItem wsItem = workspaceItemService.create(context, collection, false); + Item item = installItemService.installItem(context, wsItem); + // Simulate access conditions adding READ policy to the item + ResourcePolicy itemCustomRead = resourcePolicyService.create(context, eperson, null); + itemCustomRead.setAction(Constants.READ); + itemCustomRead.setRpType(ResourcePolicy.TYPE_CUSTOM); + // Simulate a random ADMIN action policy that might have been added manually + ResourcePolicy itemCustomAdmin = resourcePolicyService.create(context, eperson, null); + itemCustomAdmin.setAction(Constants.ADMIN); + itemCustomAdmin.setRpType(ResourcePolicy.TYPE_CUSTOM); + List customPolicies = new ArrayList<>(); + customPolicies.add(itemCustomRead); + customPolicies.add(itemCustomAdmin); + authorizeService.addPolicies(context, customPolicies, item); + // Create a bundle, this should call inheritPolicies via itemService.addBundle + Bundle bundle = bundleService.create(context, item, "THUMBNAIL"); + List newPolicies = authorizeService + .findPoliciesByDSOAndType(context, bundle, ResourcePolicy.TYPE_CUSTOM); + Assert.assertEquals("Bundle should inherit custom policy from item", 1, newPolicies.size()); + Assert.assertNotEquals("Bundle should ONLY inherit non-admin custom policy from item", + Constants.ADMIN, newPolicies.get(0).getAction()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + context.restoreAuthSystemState(); + } + } + + /** + * For other DSOs (which pass false) and for a bundle explicitly calling + * inheritPolicies(..., false), the TYPE_CUSTOM policies should not be inherited + * but other non-admin policies should be inherited as usual + */ + @Test + public void testNonInheritanceOfCustomPolicies() { + try { + context.turnOffAuthorisationSystem(); + Community community = communityService.create(null, context); + Collection collection = collectionService.create(context, community); + WorkspaceItem wsItem = workspaceItemService.create(context, collection, false); + Item item = installItemService.installItem(context, wsItem); + Bundle bundle = bundleService.create(context, item, "THUMBNAIL"); + // Simulate a custom READ policy added by access conditions step + ResourcePolicy itemCustomRead = resourcePolicyService.create(context, eperson, null); + itemCustomRead.setAction(Constants.READ); + itemCustomRead.setRpType(ResourcePolicy.TYPE_CUSTOM); + // Simulate an ordinary default read item policy inherited from collection + ResourcePolicy itemDefaultRead = resourcePolicyService.create(context, eperson, null); + itemDefaultRead.setAction(Constants.READ); + itemDefaultRead.setRpType(ResourcePolicy.TYPE_INHERITED); + List customPolicies = new ArrayList<>(); + customPolicies.add(itemCustomRead); + customPolicies.add(itemDefaultRead); + authorizeService.addPolicies(context, customPolicies, item); + // Now, inherit policies for bundle with includeCustom=false (which is how other DSOs behave) + authorizeService.inheritPolicies(context, item, bundle, false); + List newCustomPolicies = authorizeService + .findPoliciesByDSOAndType(context, bundle, ResourcePolicy.TYPE_CUSTOM); + List newInheritedPolicies = authorizeService + .findPoliciesByDSOAndType(context, bundle, ResourcePolicy.TYPE_INHERITED); + Assert.assertEquals("Bundle should not inherit custom policy from item, if false passed", + 0, newCustomPolicies.size()); + Assert.assertEquals("Bundle should inherit non-custom, non-admin policies as usual", + ResourcePolicy.TYPE_INHERITED, newInheritedPolicies.get(0).getRpType()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + context.restoreAuthSystemState(); + } + } + // // @Test // public void testIsCollectionAdmin() throws SQLException, AuthorizeException, IOException { From 53538cd3e8890f9f759535aa3d525649b5ff0a15 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 28 May 2025 17:26:13 +0200 Subject: [PATCH 240/701] [TLC-1097] ORCID external identifier sync fix Handle SELF and PART_OF identifiers properly based on configuration, work type, and identifier type (cherry picked from commit ad82b31c7466e71e5a3e8da1c5732fd3cf25f063) --- .../orcid/model/OrcidWorkFieldMapping.java | 10 ++ .../model/factory/impl/OrcidWorkFactory.java | 67 +++++++------ .../java/org/dspace/builder/ItemBuilder.java | 8 ++ .../service/OrcidEntityFactoryServiceIT.java | 99 ++++++++++++++++++- ...space-to-orcid-publication-type.properties | 3 +- dspace/config/modules/orcid.cfg | 10 +- dspace/config/spring/api/orcid-services.xml | 57 +++++++---- 7 files changed, 201 insertions(+), 53 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java index 781a9dcbd904..faefe798e92b 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java @@ -39,6 +39,7 @@ public class OrcidWorkFieldMapping { * The metadata fields related to the work external identifiers. */ private Map externalIdentifierFields = new HashMap<>(); + private Map> externalIdentifierPartOfMap = new HashMap<>(); /** * The metadata field related to the work publication date. @@ -129,6 +130,15 @@ public void setExternalIdentifierFields(String externalIdentifierFields) { this.externalIdentifierFields = parseConfigurations(externalIdentifierFields); } + public Map> getExternalIdentifierPartOfMap() { + return this.externalIdentifierPartOfMap; + } + + public void setExternalIdentifierPartOfMap( + HashMap> externalIdentifierPartOfMap) { + this.externalIdentifierPartOfMap = externalIdentifierPartOfMap; + } + public String getPublicationDateField() { return publicationDateField; } diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java index 47619b3c1d63..66fd1a717d7b 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java @@ -9,6 +9,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.orcid.jaxb.model.common.Relationship.PART_OF; import static org.orcid.jaxb.model.common.Relationship.SELF; import java.util.ArrayList; @@ -73,12 +74,12 @@ public OrcidEntityType getEntityType() { @Override public Activity createOrcidObject(Context context, Item item) { Work work = new Work(); + work.setWorkType(getWorkType(context, item)); work.setJournalTitle(getJournalTitle(context, item)); work.setWorkContributors(getWorkContributors(context, item)); work.setWorkTitle(getWorkTitle(context, item)); work.setPublicationDate(getPublicationDate(context, item)); - work.setWorkExternalIdentifiers(getWorkExternalIds(context, item)); - work.setWorkType(getWorkType(context, item)); + work.setWorkExternalIdentifiers(getWorkExternalIds(context, item, work)); work.setShortDescription(getShortDescription(context, item)); work.setLanguageCode(getLanguageCode(context, item)); work.setUrl(getUrl(context, item)); @@ -148,58 +149,62 @@ private PublicationDate getPublicationDate(Context context, Item item) { .orElse(null); } - /** - * Creates an instance of ExternalIDs from the metadata values of the given - * item, using the orcid.mapping.funding.external-ids configuration. - */ - private ExternalIDs getWorkExternalIds(Context context, Item item) { - ExternalIDs externalIdentifiers = new ExternalIDs(); - externalIdentifiers.getExternalIdentifier().addAll(getWorkSelfExternalIds(context, item)); - return externalIdentifiers; + private ExternalIDs getWorkExternalIds(Context context, Item item, Work work) { + ExternalIDs externalIDs = new ExternalIDs(); + externalIDs.getExternalIdentifier().addAll(getWorkExternalIdList(context, item, work)); + return externalIDs; } /** * Creates a list of ExternalID, one for orcid.mapping.funding.external-ids * value, taking the values from the given item. */ - private List getWorkSelfExternalIds(Context context, Item item) { + private List getWorkExternalIdList(Context context, Item item, Work work) { - List selfExternalIds = new ArrayList<>(); + List externalIds = new ArrayList<>(); Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); if (externalIdentifierFields.containsKey(SIMPLE_HANDLE_PLACEHOLDER)) { String handleType = externalIdentifierFields.get(SIMPLE_HANDLE_PLACEHOLDER); - selfExternalIds.add(getExternalId(handleType, item.getHandle(), SELF)); + ExternalID handle = new ExternalID(); + handle.setType(handleType); + handle.setValue(item.getHandle()); + handle.setRelationship(SELF); + externalIds.add(handle); } + // Resolve work type, used to determine identifier relationship type + // For version / funding relationships, we might want to use more complex + // business rules than just "work and id type" + final String workType = (work != null && work.getWorkType() != null) ? + work.getWorkType().value() : WorkType.OTHER.value(); getMetadataValues(context, item, externalIdentifierFields.keySet()).stream() - .map(this::getSelfExternalId) - .forEach(selfExternalIds::add); + .map(metadataValue -> this.getExternalId(metadataValue, workType)) + .forEach(externalIds::add); - return selfExternalIds; - } - - /** - * Creates an instance of ExternalID taking the value from the given - * metadataValue. The type of the ExternalID is calculated using the - * orcid.mapping.funding.external-ids configuration. The relationship of the - * ExternalID is SELF. - */ - private ExternalID getSelfExternalId(MetadataValue metadataValue) { - Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); - String metadataField = metadataValue.getMetadataField().toString('.'); - return getExternalId(externalIdentifierFields.get(metadataField), metadataValue.getValue(), SELF); + return externalIds; } /** * Creates an instance of ExternalID with the given type, value and * relationship. */ - private ExternalID getExternalId(String type, String value, Relationship relationship) { + private ExternalID getExternalId(MetadataValue metadataValue, String workType) { + Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); + Map> externalIdentifierPartOfMap = fieldMapping.getExternalIdentifierPartOfMap(); + String metadataField = metadataValue.getMetadataField().toString('.'); + String identifierType = externalIdentifierFields.get(metadataField); + // Default relationship type is SELF, configuration can + // override to PART_OF based on identifier and work type + Relationship relationship = SELF; + if (externalIdentifierPartOfMap.containsKey(identifierType) + && externalIdentifierPartOfMap.get(identifierType).contains(workType)) { + relationship = PART_OF; + } ExternalID externalID = new ExternalID(); - externalID.setType(type); - externalID.setValue(value); + externalID.setType(identifierType); + externalID.setValue(metadataValue.getValue()); externalID.setRelationship(relationship); return externalID; } diff --git a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java index 5e9545fcafbd..d3e4d0ff8f78 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java @@ -113,6 +113,14 @@ public ItemBuilder withScopusIdentifier(String scopus) { return addMetadataValue(item, "dc", "identifier", "scopus", scopus); } + public ItemBuilder withISSN(String issn) { + return addMetadataValue(item, "dc", "identifier", "issn", issn); + } + + public ItemBuilder withISBN(String isbn) { + return addMetadataValue(item, "dc", "identifier", "isbn", isbn); + } + public ItemBuilder withRelationFunding(String funding) { return addMetadataValue(item, "dc", "relation", "funding", funding); } diff --git a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java index 912efcfcf323..c73e7adecc41 100644 --- a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java @@ -73,6 +73,9 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData private Collection projects; + private static final String isbn = "978-0-439-02348-1"; + private static final String issn = "1234-1234X"; + @Before public void setup() { @@ -117,6 +120,7 @@ public void testWorkCreation() { .withLanguage("en_US") .withType("Book") .withIsPartOf("Journal") + .withISBN(isbn) .withDoiIdentifier("doi-id") .withScopusIdentifier("scopus-id") .build(); @@ -149,11 +153,100 @@ public void testWorkCreation() { assertThat(work.getExternalIdentifiers(), notNullValue()); List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); - assertThat(externalIds, hasSize(3)); + assertThat(externalIds, hasSize(4)); + assertThat(externalIds, has(selfExternalId("doi", "doi-id"))); + assertThat(externalIds, has(selfExternalId("eid", "scopus-id"))); + assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + // Book type should have SELF rel for ISBN + assertThat(externalIds, has(selfExternalId("isbn", isbn))); + + } + + @Test + public void testJournalArticleAndISSN() { + context.turnOffAuthorisationSystem(); + + Item publication = ItemBuilder.createItem(context, publications) + .withTitle("Test publication") + .withAuthor("Walter White") + .withAuthor("Jesse Pinkman") + .withEditor("Editor") + .withIssueDate("2021-04-30") + .withDescriptionAbstract("Publication description") + .withLanguage("en_US") + .withType("Article") + .withIsPartOf("Journal") + .withISSN(issn) + .withDoiIdentifier("doi-id") + .withScopusIdentifier("scopus-id") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, publication); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + assertThat(work.getJournalTitle(), notNullValue()); + assertThat(work.getJournalTitle().getContent(), is("Journal")); + assertThat(work.getLanguageCode(), is("en")); + assertThat(work.getPublicationDate(), matches(date("2021", "04", "30"))); + assertThat(work.getShortDescription(), is("Publication description")); + assertThat(work.getPutCode(), nullValue()); + assertThat(work.getWorkType(), is(WorkType.JOURNAL_ARTICLE)); + assertThat(work.getWorkTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle().getContent(), is("Test publication")); + assertThat(work.getWorkContributors(), notNullValue()); + assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle()))); + + List contributors = work.getWorkContributors().getContributor(); + assertThat(contributors, hasSize(3)); + assertThat(contributors, has(contributor("Walter White", AUTHOR, FIRST))); + assertThat(contributors, has(contributor("Editor", EDITOR, FIRST))); + assertThat(contributors, has(contributor("Jesse Pinkman", AUTHOR, ADDITIONAL))); + + assertThat(work.getExternalIdentifiers(), notNullValue()); + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(4)); assertThat(externalIds, has(selfExternalId("doi", "doi-id"))); assertThat(externalIds, has(selfExternalId("eid", "scopus-id"))); assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + // journal-article should have PART_OF rel for ISSN + assertThat(externalIds, has(externalId("issn", issn, Relationship.PART_OF))); + } + @Test + public void testJournalWithISSN() { + context.turnOffAuthorisationSystem(); + + Item publication = ItemBuilder.createItem(context, publications) + .withTitle("Test journal") + .withEditor("Editor") + .withType("Journal") + .withISSN(issn) + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, publication); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + assertThat(work.getWorkType(), is(WorkType.JOURNAL_ISSUE)); + assertThat(work.getWorkTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle().getContent(), is("Test journal")); + assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle()))); + + assertThat(work.getExternalIdentifiers(), notNullValue()); + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(2)); + // journal-issue should have SELF rel for ISSN + assertThat(externalIds, has(selfExternalId("issn", issn))); + assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); } @Test @@ -163,6 +256,7 @@ public void testEmptyWorkWithUnknownTypeCreation() { Item publication = ItemBuilder.createItem(context, publications) .withType("TYPE") + .withISSN(issn) .build(); context.restoreAuthSystemState(); @@ -183,8 +277,9 @@ public void testEmptyWorkWithUnknownTypeCreation() { assertThat(work.getExternalIdentifiers(), notNullValue()); List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); - assertThat(externalIds, hasSize(1)); + assertThat(externalIds, hasSize(2)); assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + assertThat(externalIds, has(externalId("issn", issn, Relationship.PART_OF))); } @Test diff --git a/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties b/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties index 953ddc60eef6..6eac57092a10 100644 --- a/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties +++ b/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties @@ -7,6 +7,7 @@ Dataset = data-set Learning\ Object = other Image = other Image,\ 3-D = other +Journal = journal-issue Map = other Musical\ Score = other Plan\ or\ blueprint = other @@ -20,4 +21,4 @@ Technical\ Report = other Thesis = other Video = other Working\ Paper = working-paper -Other = other \ No newline at end of file +Other = other diff --git a/dspace/config/modules/orcid.cfg b/dspace/config/modules/orcid.cfg index ad31371cb890..e3c021f9c631 100644 --- a/dspace/config/modules/orcid.cfg +++ b/dspace/config/modules/orcid.cfg @@ -1,4 +1,3 @@ - #------------------------------------------------------------------# #--------------------ORCID GENERIC CONFIGURATIONS------------------# #------------------------------------------------------------------# @@ -61,12 +60,18 @@ orcid.mapping.work.contributors = dc.contributor.editor::editor ##orcid.mapping.work.external-ids syntax is :: or $simple-handle:: ##The full list of available external identifiers is available here https://pub.orcid.org/v3.0/identifiers +# The identifiers need to have a relationship of SELF, PART_OF, VERSION_OF or FUNDED_BY. +# The default for most identifiers is SELF. The default for identifiers more commonly +# associated with 'parent' publciations (ISSN, ISBN) is PART_OF. +# See the map in `orcid-services.xml` +# VERSION_OF and FUNDED_BY are not currently implemented. orcid.mapping.work.external-ids = dc.identifier.doi::doi orcid.mapping.work.external-ids = dc.identifier.scopus::eid orcid.mapping.work.external-ids = dc.identifier.pmid::pmid orcid.mapping.work.external-ids = $simple-handle::handle orcid.mapping.work.external-ids = dc.identifier.isi::wosuid orcid.mapping.work.external-ids = dc.identifier.issn::issn +orcid.mapping.work.external-ids = dc.identifier.isbn::isbn ### Funding mapping ### orcid.mapping.funding.title = dc.title @@ -146,6 +151,9 @@ orcid.bulk-synchronization.max-attempts = 5 #--------------------ORCID EXTERNAL DATA MAPPING-------------------# #------------------------------------------------------------------# +# Note - the below mapping is for ORCID->DSpace imports, not for +# DSpace->ORCID exports (see orcid.mapping.work.*) + ### Work (Publication) external-data.mapping ### orcid.external-data.mapping.publication.title = dc.title diff --git a/dspace/config/spring/api/orcid-services.xml b/dspace/config/spring/api/orcid-services.xml index 6ec9be9fdf5d..c7a131832de1 100644 --- a/dspace/config/spring/api/orcid-services.xml +++ b/dspace/config/spring/api/orcid-services.xml @@ -55,24 +55,45 @@ - - - - - - - - - - - - - - - - - - + + + + + journal-article + magazine-article + newspaper-article + data-set + learning-object + other + + + + + book-chapter + book-review + other + + + + + + + + + + + + + + + + + + + + + + From 5d58cb78ead2c60a62167cba3a694e328bdd34e7 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 2 Jun 2025 18:23:34 +0200 Subject: [PATCH 241/701] [TLC-1097] Additional javadoc for ORCID sync fix (cherry picked from commit 1b864e6ca2afab76131afa9ee39152c1802eb23b) --- .../dspace/orcid/model/factory/impl/OrcidWorkFactory.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java index 66fd1a717d7b..280a5ac2155f 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java @@ -149,6 +149,10 @@ private PublicationDate getPublicationDate(Context context, Item item) { .orElse(null); } + /** + * Returns a list of external work IDs constructed in the org.orcid.jaxb + * ExternalIDs object + */ private ExternalIDs getWorkExternalIds(Context context, Item item, Work work) { ExternalIDs externalIDs = new ExternalIDs(); externalIDs.getExternalIdentifier().addAll(getWorkExternalIdList(context, item, work)); @@ -157,7 +161,7 @@ private ExternalIDs getWorkExternalIds(Context context, Item item, Work work) { /** * Creates a list of ExternalID, one for orcid.mapping.funding.external-ids - * value, taking the values from the given item. + * value, taking the values from the given item and work type. */ private List getWorkExternalIdList(Context context, Item item, Work work) { From 3bca2164be9cfd08d2e45cd766ad977aa517e397 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 28 May 2025 17:26:13 +0200 Subject: [PATCH 242/701] [TLC-1097] ORCID external identifier sync fix Handle SELF and PART_OF identifiers properly based on configuration, work type, and identifier type --- .../orcid/model/OrcidWorkFieldMapping.java | 10 ++ .../model/factory/impl/OrcidWorkFactory.java | 69 +++++++------ .../java/org/dspace/builder/ItemBuilder.java | 8 ++ .../service/OrcidEntityFactoryServiceIT.java | 99 ++++++++++++++++++- ...space-to-orcid-publication-type.properties | 3 +- dspace/config/modules/orcid.cfg | 10 +- dspace/config/spring/api/orcid-services.xml | 57 +++++++---- 7 files changed, 202 insertions(+), 54 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java index 781a9dcbd904..faefe798e92b 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidWorkFieldMapping.java @@ -39,6 +39,7 @@ public class OrcidWorkFieldMapping { * The metadata fields related to the work external identifiers. */ private Map externalIdentifierFields = new HashMap<>(); + private Map> externalIdentifierPartOfMap = new HashMap<>(); /** * The metadata field related to the work publication date. @@ -129,6 +130,15 @@ public void setExternalIdentifierFields(String externalIdentifierFields) { this.externalIdentifierFields = parseConfigurations(externalIdentifierFields); } + public Map> getExternalIdentifierPartOfMap() { + return this.externalIdentifierPartOfMap; + } + + public void setExternalIdentifierPartOfMap( + HashMap> externalIdentifierPartOfMap) { + this.externalIdentifierPartOfMap = externalIdentifierPartOfMap; + } + public String getPublicationDateField() { return publicationDateField; } diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java index 53b46d8256d1..a0835d2eb7a9 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java @@ -9,6 +9,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.orcid.jaxb.model.common.Relationship.PART_OF; import static org.orcid.jaxb.model.common.Relationship.SELF; import java.util.ArrayList; @@ -73,12 +74,12 @@ public OrcidEntityType getEntityType() { @Override public Activity createOrcidObject(Context context, Item item) { Work work = new Work(); + work.setWorkType(getWorkType(context, item)); work.setJournalTitle(getJournalTitle(context, item)); work.setWorkContributors(getWorkContributors(context, item)); work.setWorkTitle(getWorkTitle(context, item)); work.setPublicationDate(getPublicationDate(context, item)); - work.setWorkExternalIdentifiers(getWorkExternalIds(context, item)); - work.setWorkType(getWorkType(context, item)); + work.setWorkExternalIdentifiers(getWorkExternalIds(context, item, work)); work.setShortDescription(getShortDescription(context, item)); work.setLanguageCode(getLanguageCode(context, item)); work.setUrl(getUrl(context, item)); @@ -148,64 +149,68 @@ private PublicationDate getPublicationDate(Context context, Item item) { .orElse(null); } - /** - * Creates an instance of ExternalIDs from the metadata values of the given - * item, using the orcid.mapping.funding.external-ids configuration. - */ - private ExternalIDs getWorkExternalIds(Context context, Item item) { - ExternalIDs externalIdentifiers = new ExternalIDs(); - externalIdentifiers.getExternalIdentifier().addAll(getWorkSelfExternalIds(context, item)); - return externalIdentifiers; + private ExternalIDs getWorkExternalIds(Context context, Item item, Work work) { + ExternalIDs externalIDs = new ExternalIDs(); + externalIDs.getExternalIdentifier().addAll(getWorkExternalIdList(context, item, work)); + return externalIDs; } /** * Creates a list of ExternalID, one for orcid.mapping.funding.external-ids * value, taking the values from the given item. */ - private List getWorkSelfExternalIds(Context context, Item item) { + private List getWorkExternalIdList(Context context, Item item, Work work) { - List selfExternalIds = new ArrayList(); + List externalIds = new ArrayList<>(); Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); if (externalIdentifierFields.containsKey(SIMPLE_HANDLE_PLACEHOLDER)) { String handleType = externalIdentifierFields.get(SIMPLE_HANDLE_PLACEHOLDER); - selfExternalIds.add(getExternalId(handleType, item.getHandle(), SELF)); + ExternalID handle = new ExternalID(); + handle.setType(handleType); + handle.setValue(item.getHandle()); + handle.setRelationship(SELF); + externalIds.add(handle); } + // Resolve work type, used to determine identifier relationship type + // For version / funding relationships, we might want to use more complex + // business rules than just "work and id type" + final String workType = (work != null && work.getWorkType() != null) ? + work.getWorkType().value() : WorkType.OTHER.value(); getMetadataValues(context, item, externalIdentifierFields.keySet()).stream() - .map(this::getSelfExternalId) - .forEach(selfExternalIds::add); + .map(metadataValue -> this.getExternalId(metadataValue, workType)) + .forEach(externalIds::add); - return selfExternalIds; - } - - /** - * Creates an instance of ExternalID taking the value from the given - * metadataValue. The type of the ExternalID is calculated using the - * orcid.mapping.funding.external-ids configuration. The relationship of the - * ExternalID is SELF. - */ - private ExternalID getSelfExternalId(MetadataValue metadataValue) { - Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); - String metadataField = metadataValue.getMetadataField().toString('.'); - return getExternalId(externalIdentifierFields.get(metadataField), metadataValue.getValue(), SELF); + return externalIds; } /** * Creates an instance of ExternalID with the given type, value and * relationship. */ - private ExternalID getExternalId(String type, String value, Relationship relationship) { + private ExternalID getExternalId(MetadataValue metadataValue, String workType) { + Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); + Map> externalIdentifierPartOfMap = fieldMapping.getExternalIdentifierPartOfMap(); + String metadataField = metadataValue.getMetadataField().toString('.'); + String identifierType = externalIdentifierFields.get(metadataField); + // Default relationship type is SELF, configuration can + // override to PART_OF based on identifier and work type + Relationship relationship = SELF; + if (externalIdentifierPartOfMap.containsKey(identifierType) + && externalIdentifierPartOfMap.get(identifierType).contains(workType)) { + relationship = PART_OF; + } ExternalID externalID = new ExternalID(); - externalID.setType(type); - externalID.setValue(value); + externalID.setType(identifierType); + externalID.setValue(metadataValue.getValue()); externalID.setRelationship(relationship); return externalID; } /** - * Creates an instance of WorkType from the given item, taking the value fom the + * Creates an instance of WorkType from the given item, taking the value from the * configured metadata field (orcid.mapping.work.type). */ private WorkType getWorkType(Context context, Item item) { diff --git a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java index 3e5ab0f38f5b..9918b5096858 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java @@ -112,6 +112,14 @@ public ItemBuilder withScopusIdentifier(String scopus) { return addMetadataValue(item, "dc", "identifier", "scopus", scopus); } + public ItemBuilder withISSN(String issn) { + return addMetadataValue(item, "dc", "identifier", "issn", issn); + } + + public ItemBuilder withISBN(String isbn) { + return addMetadataValue(item, "dc", "identifier", "isbn", isbn); + } + public ItemBuilder withRelationFunding(String funding) { return addMetadataValue(item, "dc", "relation", "funding", funding); } diff --git a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java index 17bc6ee531c3..33f5cc3102ec 100644 --- a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java @@ -73,6 +73,9 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData private Collection projects; + private static final String isbn = "978-0-439-02348-1"; + private static final String issn = "1234-1234X"; + @Before public void setup() { @@ -117,6 +120,7 @@ public void testWorkCreation() { .withLanguage("en_US") .withType("Book") .withIsPartOf("Journal") + .withISBN(isbn) .withDoiIdentifier("doi-id") .withScopusIdentifier("scopus-id") .build(); @@ -149,11 +153,100 @@ public void testWorkCreation() { assertThat(work.getExternalIdentifiers(), notNullValue()); List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); - assertThat(externalIds, hasSize(3)); + assertThat(externalIds, hasSize(4)); + assertThat(externalIds, has(selfExternalId("doi", "doi-id"))); + assertThat(externalIds, has(selfExternalId("eid", "scopus-id"))); + assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + // Book type should have SELF rel for ISBN + assertThat(externalIds, has(selfExternalId("isbn", isbn))); + + } + + @Test + public void testJournalArticleAndISSN() { + context.turnOffAuthorisationSystem(); + + Item publication = ItemBuilder.createItem(context, publications) + .withTitle("Test publication") + .withAuthor("Walter White") + .withAuthor("Jesse Pinkman") + .withEditor("Editor") + .withIssueDate("2021-04-30") + .withDescriptionAbstract("Publication description") + .withLanguage("en_US") + .withType("Article") + .withIsPartOf("Journal") + .withISSN(issn) + .withDoiIdentifier("doi-id") + .withScopusIdentifier("scopus-id") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, publication); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + assertThat(work.getJournalTitle(), notNullValue()); + assertThat(work.getJournalTitle().getContent(), is("Journal")); + assertThat(work.getLanguageCode(), is("en")); + assertThat(work.getPublicationDate(), matches(date("2021", "04", "30"))); + assertThat(work.getShortDescription(), is("Publication description")); + assertThat(work.getPutCode(), nullValue()); + assertThat(work.getWorkType(), is(WorkType.JOURNAL_ARTICLE)); + assertThat(work.getWorkTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle().getContent(), is("Test publication")); + assertThat(work.getWorkContributors(), notNullValue()); + assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle()))); + + List contributors = work.getWorkContributors().getContributor(); + assertThat(contributors, hasSize(3)); + assertThat(contributors, has(contributor("Walter White", AUTHOR, FIRST))); + assertThat(contributors, has(contributor("Editor", EDITOR, FIRST))); + assertThat(contributors, has(contributor("Jesse Pinkman", AUTHOR, ADDITIONAL))); + + assertThat(work.getExternalIdentifiers(), notNullValue()); + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(4)); assertThat(externalIds, has(selfExternalId("doi", "doi-id"))); assertThat(externalIds, has(selfExternalId("eid", "scopus-id"))); assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + // journal-article should have PART_OF rel for ISSN + assertThat(externalIds, has(externalId("issn", issn, Relationship.PART_OF))); + } + @Test + public void testJournalWithISSN() { + context.turnOffAuthorisationSystem(); + + Item publication = ItemBuilder.createItem(context, publications) + .withTitle("Test journal") + .withEditor("Editor") + .withType("Journal") + .withISSN(issn) + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, publication); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + assertThat(work.getWorkType(), is(WorkType.JOURNAL_ISSUE)); + assertThat(work.getWorkTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle().getContent(), is("Test journal")); + assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle()))); + + assertThat(work.getExternalIdentifiers(), notNullValue()); + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(2)); + // journal-issue should have SELF rel for ISSN + assertThat(externalIds, has(selfExternalId("issn", issn))); + assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); } @Test @@ -163,6 +256,7 @@ public void testEmptyWorkWithUnknownTypeCreation() { Item publication = ItemBuilder.createItem(context, publications) .withType("TYPE") + .withISSN(issn) .build(); context.restoreAuthSystemState(); @@ -183,8 +277,9 @@ public void testEmptyWorkWithUnknownTypeCreation() { assertThat(work.getExternalIdentifiers(), notNullValue()); List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); - assertThat(externalIds, hasSize(1)); + assertThat(externalIds, hasSize(2)); assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); + assertThat(externalIds, has(externalId("issn", issn, Relationship.PART_OF))); } @Test diff --git a/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties b/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties index 953ddc60eef6..6eac57092a10 100644 --- a/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties +++ b/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-publication-type.properties @@ -7,6 +7,7 @@ Dataset = data-set Learning\ Object = other Image = other Image,\ 3-D = other +Journal = journal-issue Map = other Musical\ Score = other Plan\ or\ blueprint = other @@ -20,4 +21,4 @@ Technical\ Report = other Thesis = other Video = other Working\ Paper = working-paper -Other = other \ No newline at end of file +Other = other diff --git a/dspace/config/modules/orcid.cfg b/dspace/config/modules/orcid.cfg index 93da0a5f4cb9..7eb2af9b8088 100644 --- a/dspace/config/modules/orcid.cfg +++ b/dspace/config/modules/orcid.cfg @@ -1,4 +1,3 @@ - #------------------------------------------------------------------# #--------------------ORCID GENERIC CONFIGURATIONS------------------# #------------------------------------------------------------------# @@ -58,12 +57,18 @@ orcid.mapping.work.contributors = dc.contributor.editor::editor ##orcid.mapping.work.external-ids syntax is :: or $simple-handle:: ##The full list of available external identifiers is available here https://pub.orcid.org/v3.0/identifiers +# The identifiers need to have a relationship of SELF, PART_OF, VERSION_OF or FUNDED_BY. +# The default for most identifiers is SELF. The default for identifiers more commonly +# associated with 'parent' publciations (ISSN, ISBN) is PART_OF. +# See the map in `orcid-services.xml` +# VERSION_OF and FUNDED_BY are not currently implemented. orcid.mapping.work.external-ids = dc.identifier.doi::doi orcid.mapping.work.external-ids = dc.identifier.scopus::eid orcid.mapping.work.external-ids = dc.identifier.pmid::pmid orcid.mapping.work.external-ids = $simple-handle::handle orcid.mapping.work.external-ids = dc.identifier.isi::wosuid orcid.mapping.work.external-ids = dc.identifier.issn::issn +orcid.mapping.work.external-ids = dc.identifier.isbn::isbn ### Funding mapping ### orcid.mapping.funding.title = dc.title @@ -141,6 +146,9 @@ orcid.bulk-synchronization.max-attempts = 5 #--------------------ORCID EXTERNAL DATA MAPPING-------------------# #------------------------------------------------------------------# +# Note - the below mapping is for ORCID->DSpace imports, not for +# DSpace->ORCID exports (see orcid.mapping.work.*) + ### Work (Publication) external-data.mapping ### orcid.external-data.mapping.publication.title = dc.title diff --git a/dspace/config/spring/api/orcid-services.xml b/dspace/config/spring/api/orcid-services.xml index eb31acb29c4d..a2f914d0ad2a 100644 --- a/dspace/config/spring/api/orcid-services.xml +++ b/dspace/config/spring/api/orcid-services.xml @@ -55,24 +55,45 @@ - - - - - - - - - - - - - - - - - - + + + + + journal-article + magazine-article + newspaper-article + data-set + learning-object + other + + + + + book-chapter + book-review + other + + + + + + + + + + + + + + + + + + + + + + From af654e57f2d09e019d6c890c759440f27ebc6c25 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 2 Jun 2025 18:23:34 +0200 Subject: [PATCH 243/701] [TLC-1097] Additional javadoc for ORCID sync fix --- .../dspace/orcid/model/factory/impl/OrcidWorkFactory.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java index a0835d2eb7a9..890dd0c6ed9d 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java @@ -149,6 +149,10 @@ private PublicationDate getPublicationDate(Context context, Item item) { .orElse(null); } + /** + * Returns a list of external work IDs constructed in the org.orcid.jaxb + * ExternalIDs object + */ private ExternalIDs getWorkExternalIds(Context context, Item item, Work work) { ExternalIDs externalIDs = new ExternalIDs(); externalIDs.getExternalIdentifier().addAll(getWorkExternalIdList(context, item, work)); @@ -157,7 +161,7 @@ private ExternalIDs getWorkExternalIds(Context context, Item item, Work work) { /** * Creates a list of ExternalID, one for orcid.mapping.funding.external-ids - * value, taking the values from the given item. + * value, taking the values from the given item and work type. */ private List getWorkExternalIdList(Context context, Item item, Work work) { From b8cbaa053d5fd95ca691c920447668f9346240ab Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 08:50:41 -0500 Subject: [PATCH 244/701] Add a deposit integration test for SWORDv1 based on the similar SWORDv2 test. (cherry picked from commit 0589011849cf4c7ac7a67d6dfc44839e11047980) --- .../java/org/dspace/app/sword/Swordv1IT.java | 98 +++++++++++++++++-- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java index 24244e1773e6..0b866659edd7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java @@ -10,16 +10,30 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import java.nio.file.Path; +import java.util.List; + import org.dspace.app.rest.test.AbstractWebClientIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.content.Collection; import org.dspace.services.ConfigurationService; +import org.hamcrest.MatcherAssert; import org.junit.Assume; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestPropertySource; @@ -45,6 +59,9 @@ public class Swordv1IT extends AbstractWebClientIntegrationTest { private final String DEPOSIT_PATH = "/sword/deposit"; private final String MEDIA_LINK_PATH = "/sword/media-link"; + // ATOM Content type returned by SWORDv1 + private final String ATOM_CONTENT_TYPE = "application/atom+xml;charset=UTF-8"; + @Before public void onlyRunIfConfigExists() { // These integration tests REQUIRE that SWORDWebConfig is found/available (as this class deploys SWORD) @@ -93,10 +110,76 @@ public void depositUnauthorizedTest() throws Exception { } @Test - @Ignore public void depositTest() throws Exception { - // TODO: Actually test a full deposit via SWORD. - // Currently, we are just ensuring the /deposit endpoint exists (see above) and isn't throwing a 404 + context.turnOffAuthorisationSystem(); + // Create a top level community and one Collection + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + // Make sure our Collection allows the "eperson" user to submit into it + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Test SWORDv1 Collection") + .withSubmitterGroup(eperson) + .build(); + // Above changes MUST be committed to the database for SWORDv2 to see them. + context.commit(); + context.restoreAuthSystemState(); + + // Specify zip file + // NOTE: We are using the same "example.zip" as SWORDv2IT because that same ZIP is valid for both v1 and v2 + FileSystemResource zipFile = new FileSystemResource(Path.of("src", "test", "resources", "org", + "dspace", "app", "sword2", "example.zip")); + + // Add required headers + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.valueOf("application/zip")); + headers.setContentDisposition(ContentDisposition.attachment().filename("example.zip").build()); + headers.set("X-Packaging", "http://purl.org/net/sword-types/METSDSpaceSIP"); + headers.setAccept(List.of(MediaType.APPLICATION_ATOM_XML)); + + //---- + // STEP 1: Verify upload/submit via SWORDv1 works + //---- + // Send POST to upload Zip file via SWORD + ResponseEntity response = postResponseAsString(DEPOSIT_PATH + "/" + collection.getHandle(), + eperson.getEmail(), password, + new HttpEntity<>(zipFile.getContentAsByteArray(), + headers)); + + // Expect a 201 CREATED response with ATOM content returned + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + assertEquals(ATOM_CONTENT_TYPE, response.getHeaders().getContentType().toString()); + + // MUST return a "Location" header which is the "/sword/media-link/*" URI of the zip file bitstream within + // the created item (e.g. /sword/media-link/[handle-prefix]/[handle-suffix]/bitstream/[uuid]) + assertNotNull(response.getHeaders().getLocation()); + String mediaLink = response.getHeaders().getLocation().toString(); + + // Body should include the SWORD version in generator tag + MatcherAssert.assertThat(response.getBody(), + containsString("")); + // Verify Item title also is returned in the body + MatcherAssert.assertThat(response.getBody(), containsString("Attempts to detect retrotransposition")); + + //---- + // STEP 2: Verify /media-link access works + //---- + // Media-Link URI should work when requested by the EPerson who did the deposit + HttpHeaders authHeaders = new HttpHeaders(); + authHeaders.setBasicAuth(eperson.getEmail(), password); + RequestEntity request = RequestEntity.get(mediaLink) + .accept(MediaType.valueOf("application/atom+xml")) + .headers(authHeaders) + .build(); + response = responseAsString(request); + + // Expect a 200 response with ATOM feed content returned + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(ATOM_CONTENT_TYPE, response.getHeaders().getContentType().toString()); + // Body should include a link to the zip bitstream in the newly created Item + // This just verifies "example.zip" exists in the body. + MatcherAssert.assertThat(response.getBody(), containsString("example.zip")); } @Test @@ -105,13 +188,8 @@ public void mediaLinkUnauthorizedTest() throws Exception { ResponseEntity response = getResponseAsString(MEDIA_LINK_PATH); // Expect a 401 response code assertThat(response.getStatusCode(), equalTo(HttpStatus.UNAUTHORIZED)); - } - @Test - @Ignore - public void mediaLinkTest() throws Exception { - // TODO: Actually test a /media-link request. - // Currently, we are just ensuring the /media-link endpoint exists (see above) and isn't throwing a 404 + //NOTE: An authorized /media-link test is performed in depositTest() above. } } From ea6e025519ae6c2f19b4d8ac19461e4881ee298e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 08:51:16 -0500 Subject: [PATCH 245/701] Fix WRITE permissions error when ingesting a new Item. Do not call "updateDSpaceObject" after calling "finishCreateItem" as the latter saves the object and removes submitter privileges from it. (cherry picked from commit c2d05891ab32596b326d78d2e4b5ff2372d94ca2) --- .../content/packager/AbstractMETSIngester.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index 77236be9d525..0ab5ac71cda5 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -498,8 +498,11 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // Finish creating the item. This actually assigns the handle, // and will either install item immediately or start a workflow, based on params PackageUtils.finishCreateItem(context, wsi, handle, params); + } else { + // We should have a workspace item during ingest, so this code is only here for safety. + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } - } else if (type == Constants.COLLECTION || type == Constants.COMMUNITY) { // Add logo if one is referenced from manifest addContainerLogo(context, dso, manifest, pkgFile, params); @@ -513,6 +516,9 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // (this allows subclasses to do some final validation / changes as // necessary) finishObject(context, dso, params); + + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } else if (type == Constants.SITE) { // Do nothing by default -- Crosswalks will handle anything necessary to ingest at Site-level @@ -520,18 +526,15 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // (this allows subclasses to do some final validation / changes as // necessary) finishObject(context, dso, params); + + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } else { throw new PackageValidationException( "Unknown DSpace Object type in package, type=" + String.valueOf(type)); } - // -- Step 6 -- - // Finish things up! - - // Update the object to make sure all changes are committed - PackageUtils.updateDSpaceObject(context, dso); - return dso; } From ec83eb612f4e383da4f5b560ba65200e88750471 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 08:50:41 -0500 Subject: [PATCH 246/701] Add a deposit integration test for SWORDv1 based on the similar SWORDv2 test. (cherry picked from commit 0589011849cf4c7ac7a67d6dfc44839e11047980) --- .../java/org/dspace/app/sword/Swordv1IT.java | 98 +++++++++++++++++-- 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java index 24244e1773e6..0b866659edd7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java @@ -10,16 +10,30 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import java.nio.file.Path; +import java.util.List; + import org.dspace.app.rest.test.AbstractWebClientIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.content.Collection; import org.dspace.services.ConfigurationService; +import org.hamcrest.MatcherAssert; import org.junit.Assume; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.test.context.TestPropertySource; @@ -45,6 +59,9 @@ public class Swordv1IT extends AbstractWebClientIntegrationTest { private final String DEPOSIT_PATH = "/sword/deposit"; private final String MEDIA_LINK_PATH = "/sword/media-link"; + // ATOM Content type returned by SWORDv1 + private final String ATOM_CONTENT_TYPE = "application/atom+xml;charset=UTF-8"; + @Before public void onlyRunIfConfigExists() { // These integration tests REQUIRE that SWORDWebConfig is found/available (as this class deploys SWORD) @@ -93,10 +110,76 @@ public void depositUnauthorizedTest() throws Exception { } @Test - @Ignore public void depositTest() throws Exception { - // TODO: Actually test a full deposit via SWORD. - // Currently, we are just ensuring the /deposit endpoint exists (see above) and isn't throwing a 404 + context.turnOffAuthorisationSystem(); + // Create a top level community and one Collection + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + // Make sure our Collection allows the "eperson" user to submit into it + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Test SWORDv1 Collection") + .withSubmitterGroup(eperson) + .build(); + // Above changes MUST be committed to the database for SWORDv2 to see them. + context.commit(); + context.restoreAuthSystemState(); + + // Specify zip file + // NOTE: We are using the same "example.zip" as SWORDv2IT because that same ZIP is valid for both v1 and v2 + FileSystemResource zipFile = new FileSystemResource(Path.of("src", "test", "resources", "org", + "dspace", "app", "sword2", "example.zip")); + + // Add required headers + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.valueOf("application/zip")); + headers.setContentDisposition(ContentDisposition.attachment().filename("example.zip").build()); + headers.set("X-Packaging", "http://purl.org/net/sword-types/METSDSpaceSIP"); + headers.setAccept(List.of(MediaType.APPLICATION_ATOM_XML)); + + //---- + // STEP 1: Verify upload/submit via SWORDv1 works + //---- + // Send POST to upload Zip file via SWORD + ResponseEntity response = postResponseAsString(DEPOSIT_PATH + "/" + collection.getHandle(), + eperson.getEmail(), password, + new HttpEntity<>(zipFile.getContentAsByteArray(), + headers)); + + // Expect a 201 CREATED response with ATOM content returned + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + assertEquals(ATOM_CONTENT_TYPE, response.getHeaders().getContentType().toString()); + + // MUST return a "Location" header which is the "/sword/media-link/*" URI of the zip file bitstream within + // the created item (e.g. /sword/media-link/[handle-prefix]/[handle-suffix]/bitstream/[uuid]) + assertNotNull(response.getHeaders().getLocation()); + String mediaLink = response.getHeaders().getLocation().toString(); + + // Body should include the SWORD version in generator tag + MatcherAssert.assertThat(response.getBody(), + containsString("")); + // Verify Item title also is returned in the body + MatcherAssert.assertThat(response.getBody(), containsString("Attempts to detect retrotransposition")); + + //---- + // STEP 2: Verify /media-link access works + //---- + // Media-Link URI should work when requested by the EPerson who did the deposit + HttpHeaders authHeaders = new HttpHeaders(); + authHeaders.setBasicAuth(eperson.getEmail(), password); + RequestEntity request = RequestEntity.get(mediaLink) + .accept(MediaType.valueOf("application/atom+xml")) + .headers(authHeaders) + .build(); + response = responseAsString(request); + + // Expect a 200 response with ATOM feed content returned + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(ATOM_CONTENT_TYPE, response.getHeaders().getContentType().toString()); + // Body should include a link to the zip bitstream in the newly created Item + // This just verifies "example.zip" exists in the body. + MatcherAssert.assertThat(response.getBody(), containsString("example.zip")); } @Test @@ -105,13 +188,8 @@ public void mediaLinkUnauthorizedTest() throws Exception { ResponseEntity response = getResponseAsString(MEDIA_LINK_PATH); // Expect a 401 response code assertThat(response.getStatusCode(), equalTo(HttpStatus.UNAUTHORIZED)); - } - @Test - @Ignore - public void mediaLinkTest() throws Exception { - // TODO: Actually test a /media-link request. - // Currently, we are just ensuring the /media-link endpoint exists (see above) and isn't throwing a 404 + //NOTE: An authorized /media-link test is performed in depositTest() above. } } From 4091b27370671ee4eebe722310646b9cac59fd56 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 08:51:16 -0500 Subject: [PATCH 247/701] Fix WRITE permissions error when ingesting a new Item. Do not call "updateDSpaceObject" after calling "finishCreateItem" as the latter saves the object and removes submitter privileges from it. (cherry picked from commit c2d05891ab32596b326d78d2e4b5ff2372d94ca2) --- .../content/packager/AbstractMETSIngester.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index 77236be9d525..0ab5ac71cda5 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -498,8 +498,11 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // Finish creating the item. This actually assigns the handle, // and will either install item immediately or start a workflow, based on params PackageUtils.finishCreateItem(context, wsi, handle, params); + } else { + // We should have a workspace item during ingest, so this code is only here for safety. + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } - } else if (type == Constants.COLLECTION || type == Constants.COMMUNITY) { // Add logo if one is referenced from manifest addContainerLogo(context, dso, manifest, pkgFile, params); @@ -513,6 +516,9 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // (this allows subclasses to do some final validation / changes as // necessary) finishObject(context, dso, params); + + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } else if (type == Constants.SITE) { // Do nothing by default -- Crosswalks will handle anything necessary to ingest at Site-level @@ -520,18 +526,15 @@ protected DSpaceObject ingestObject(Context context, DSpaceObject parent, // (this allows subclasses to do some final validation / changes as // necessary) finishObject(context, dso, params); + + // Update the object to make sure all changes are committed + PackageUtils.updateDSpaceObject(context, dso); } else { throw new PackageValidationException( "Unknown DSpace Object type in package, type=" + String.valueOf(type)); } - // -- Step 6 -- - // Finish things up! - - // Update the object to make sure all changes are committed - PackageUtils.updateDSpaceObject(context, dso); - return dso; } From 05af8605533c6eac920c42dde43c65f4fbc6870e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 12:19:14 -0500 Subject: [PATCH 248/701] getContentAsByteArray() didn't exist in Spring 5. Use readAllBytes() from InputStream instead. --- .../src/test/java/org/dspace/app/sword/Swordv1IT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java index 0b866659edd7..29ea05dacabf 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java @@ -143,7 +143,7 @@ public void depositTest() throws Exception { // Send POST to upload Zip file via SWORD ResponseEntity response = postResponseAsString(DEPOSIT_PATH + "/" + collection.getHandle(), eperson.getEmail(), password, - new HttpEntity<>(zipFile.getContentAsByteArray(), + new HttpEntity<>(zipFile.getInputStream().readAllBytes(), headers)); // Expect a 201 CREATED response with ATOM content returned From 56b38bb26d841cb20483d169080b221a6c1a56bf Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 6 Aug 2025 13:00:11 +0200 Subject: [PATCH 249/701] fix: uses dc.source for bitstream originalName tag Refs: DURACOM-391, #1130 (cherry picked from commit f5c0c17c663c494709666e64d594598b19db876b) --- dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java index b32983581321..8f8b7380307a 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java @@ -142,7 +142,7 @@ private static Element createBundlesElement(Context context, Item item) throws S bitstream.getField().add(createValue("name", name)); } if (oname != null) { - bitstream.getField().add(createValue("originalName", name)); + bitstream.getField().add(createValue("originalName", oname)); } if (description != null) { bitstream.getField().add(createValue("description", description)); From a972ecba51d247a8cbb9eceb668c8e6021a361fb Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 6 Aug 2025 13:00:11 +0200 Subject: [PATCH 250/701] fix: uses dc.source for bitstream originalName tag Refs: DURACOM-391, #1130 (cherry picked from commit f5c0c17c663c494709666e64d594598b19db876b) --- dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java index 40a193ea2905..c3364fb44259 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java @@ -143,7 +143,7 @@ private static Element createBundlesElement(Context context, Item item) throws S bitstream.getField().add(createValue("name", name)); } if (oname != null) { - bitstream.getField().add(createValue("originalName", name)); + bitstream.getField().add(createValue("originalName", oname)); } if (description != null) { bitstream.getField().add(createValue("description", description)); From 558f0c7beab21360e593963e422dfc72f8cab2c0 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Fri, 14 Mar 2025 13:30:22 -0400 Subject: [PATCH 251/701] Fix checkpointing for checksum checking Fixes the checkpointing for checksum checking by actually committing the changes to the database. Replacing "uncacheEntity" with "commit", as testing has shown that this is necessary to ensure that the checksum status is properly updated in the most_recent_checksum" table when checksum process is interrupted. The provided integration test fails if the checksums of bitstreams that were checked before the interruption are not properly recorded in the database. (cherry picked from commit 502b655ef6041381eb15833a82f03054894c96bb) --- .../org/dspace/checker/CheckerCommand.java | 2 +- .../org/dspace/checker/ChecksumCheckerIT.java | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java diff --git a/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java b/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java index a12ac3b98a2e..9ad9f553b445 100644 --- a/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java +++ b/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java @@ -131,7 +131,7 @@ public void process() throws SQLException { collector.collect(context, info); } - context.uncacheEntity(bitstream); + context.commit(); bitstream = dispatcher.next(); } } diff --git a/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java new file mode 100644 index 000000000000..7653d518b196 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java @@ -0,0 +1,192 @@ +/** + * 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.checker; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.SQLException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.checker.factory.CheckerServiceFactory; +import org.dspace.checker.service.ChecksumHistoryService; +import org.dspace.checker.service.MostRecentChecksumService; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ChecksumCheckerIT extends AbstractIntegrationTestWithDatabase { + protected List bitstreams; + protected MostRecentChecksumService checksumService = + CheckerServiceFactory.getInstance().getMostRecentChecksumService(); + + @Before + public void setup() throws Exception { + context.turnOffAuthorisationSystem(); + + Community parentCommunity = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .build(); + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item") + .build(); + + int numBitstreams = 3; + bitstreams = new ArrayList<>(); + for (int i = 0; i < numBitstreams; i++) { + String content = "Test bitstream " + i; + bitstreams.add( + BitstreamBuilder.createBitstream( + context, item, IOUtils.toInputStream(content, UTF_8) + ).build() + ); + } + + context.restoreAuthSystemState(); + + // Call the "updateMissingBitstreams" method so that the test bitstreams + // already have checksums in the past when CheckerCommand runs. + // Otherwise, the CheckerCommand will simply update the test + // bitstreams without going through the BitstreamDispatcher. + checksumService = CheckerServiceFactory.getInstance().getMostRecentChecksumService(); + checksumService.updateMissingBitstreams(context); + + // The "updateMissingBitstreams" method updates the test bitstreams in + // a random order. To verify that the expected bitstreams were + // processed, reset the timestamps so that the bitstreams are + // checked in a specific order (oldest first). + Instant checksumInstant = Instant.ofEpochMilli(0); + for (Bitstream bitstream: bitstreams) { + MostRecentChecksum mrc = checksumService.findByBitstream(context, bitstream); + mrc.setProcessStartDate(checksumInstant); + mrc.setProcessEndDate(checksumInstant); + checksumInstant = checksumInstant.plusSeconds(10); + } + context.commit(); + } + + @After + public void cleanUp() throws SQLException { + // Need to clean up ChecksumHistory because of a referential integrity + // constraint violation between the most_recent_checksum table and + // bitstream tables + ChecksumHistoryService checksumHistoryService = CheckerServiceFactory.getInstance().getChecksumHistoryService(); + + for (Bitstream bitstream: bitstreams) { + checksumHistoryService.deleteByBitstream(context, bitstream); + } + } + + @Test + public void testChecksumsRecordedWhenProcesingIsInterrupted() throws SQLException { + CheckerCommand checker = new CheckerCommand(context); + + // The start date to use for the checker process + Instant checkerStartDate = Instant.now(); + + // Verify that all checksums are before the checker start date + for (Bitstream bitstream: bitstreams) { + MostRecentChecksum checksum = checksumService.findByBitstream(context, bitstream); + Instant lastChecksumDate = checksum.getProcessStartDate(); + assertTrue("lastChecksumDate (" + lastChecksumDate + ") <= checkerStartDate (" + checkerStartDate + ")", + lastChecksumDate.isBefore(checkerStartDate)); + } + + // Dispatcher that throws an exception when a third bitstream is + // retrieved. + BitstreamDispatcher dispatcher = new ExpectionThrowingDispatcher( + context, checkerStartDate, false, 2); + checker.setDispatcher(dispatcher); + + + // Run the checksum checker + checker.setProcessStartDate(checkerStartDate); + try { + checker.process(); + fail("SQLException should have been thrown"); + } catch (SQLException sqle) { + // Rollback any pending transaction + context.rollback(); + } + + // Verify that the checksums of the first two bitstreams (that were + // processed before the exception) have been successfully recorded in + // the database, while the third bitstream was not updated. + int bitstreamCount = 0; + for (Bitstream bitstream: bitstreams) { + MostRecentChecksum checksum = checksumService.findByBitstream(context, bitstream); + Instant lastChecksumDate = checksum.getProcessStartDate(); + + bitstreamCount = bitstreamCount + 1; + if (bitstreamCount <= 2) { + assertTrue("lastChecksumDate (" + lastChecksumDate + ") <= checkerStartDate (" + checkerStartDate + ")", + lastChecksumDate.isAfter(checkerStartDate)); + } else { + assertTrue("lastChecksumDate (" + lastChecksumDate + ") >= checkerStartDate (" + checkerStartDate + ")", + lastChecksumDate.isBefore(checkerStartDate)); + } + } + } + + /** + * Subclass of SimpleDispatcher that only allows a limited number of "next" + * class before throwing a SQLException. + */ + class ExpectionThrowingDispatcher extends SimpleDispatcher { + // The number of "next" calls to allow before throwing a SQLException + protected int maxNextCalls; + + // The number of "next" method calls seen so far. + protected int numNextCalls = 0; + + /** + * Constructor. + * + * @param context Context + * @param startTime timestamp for beginning of checker process + * @param looping indicates whether checker should loop infinitely + * through most_recent_checksum table + * @param maxNextCalls the number of "next" method calls to allow before + * throwing a SQLException. + */ + public ExpectionThrowingDispatcher(Context context, Instant startTime, boolean looping, int maxNextCalls) { + super(context, startTime, looping); + this.maxNextCalls = maxNextCalls; + } + + /** + * Selects the next candidate bitstream. + * + * After "maxNextClass" number of calls, this method throws a + * SQLException. + * + * @throws SQLException if database error + */ + @Override + public synchronized Bitstream next() throws SQLException { + numNextCalls = numNextCalls + 1; + if (numNextCalls > maxNextCalls) { + throw new SQLException("Max 'next' method calls exceeded"); + } + return super.next(); + } + } +} From e0cf6c717f623386eb016e5018bfd781936ed622 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Fri, 14 Mar 2025 13:30:22 -0400 Subject: [PATCH 252/701] Fix checkpointing for checksum checking Fixes the checkpointing for checksum checking by actually committing the changes to the database. Replacing "uncacheEntity" with "commit", as testing has shown that this is necessary to ensure that the checksum status is properly updated in the most_recent_checksum" table when checksum process is interrupted. The provided integration test fails if the checksums of bitstreams that were checked before the interruption are not properly recorded in the database. (cherry picked from commit 502b655ef6041381eb15833a82f03054894c96bb) --- .../org/dspace/checker/CheckerCommand.java | 2 +- .../org/dspace/checker/ChecksumCheckerIT.java | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java diff --git a/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java b/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java index a12ac3b98a2e..9ad9f553b445 100644 --- a/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java +++ b/dspace-api/src/main/java/org/dspace/checker/CheckerCommand.java @@ -131,7 +131,7 @@ public void process() throws SQLException { collector.collect(context, info); } - context.uncacheEntity(bitstream); + context.commit(); bitstream = dispatcher.next(); } } diff --git a/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java new file mode 100644 index 000000000000..7653d518b196 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java @@ -0,0 +1,192 @@ +/** + * 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.checker; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.SQLException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.checker.factory.CheckerServiceFactory; +import org.dspace.checker.service.ChecksumHistoryService; +import org.dspace.checker.service.MostRecentChecksumService; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ChecksumCheckerIT extends AbstractIntegrationTestWithDatabase { + protected List bitstreams; + protected MostRecentChecksumService checksumService = + CheckerServiceFactory.getInstance().getMostRecentChecksumService(); + + @Before + public void setup() throws Exception { + context.turnOffAuthorisationSystem(); + + Community parentCommunity = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .build(); + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item") + .build(); + + int numBitstreams = 3; + bitstreams = new ArrayList<>(); + for (int i = 0; i < numBitstreams; i++) { + String content = "Test bitstream " + i; + bitstreams.add( + BitstreamBuilder.createBitstream( + context, item, IOUtils.toInputStream(content, UTF_8) + ).build() + ); + } + + context.restoreAuthSystemState(); + + // Call the "updateMissingBitstreams" method so that the test bitstreams + // already have checksums in the past when CheckerCommand runs. + // Otherwise, the CheckerCommand will simply update the test + // bitstreams without going through the BitstreamDispatcher. + checksumService = CheckerServiceFactory.getInstance().getMostRecentChecksumService(); + checksumService.updateMissingBitstreams(context); + + // The "updateMissingBitstreams" method updates the test bitstreams in + // a random order. To verify that the expected bitstreams were + // processed, reset the timestamps so that the bitstreams are + // checked in a specific order (oldest first). + Instant checksumInstant = Instant.ofEpochMilli(0); + for (Bitstream bitstream: bitstreams) { + MostRecentChecksum mrc = checksumService.findByBitstream(context, bitstream); + mrc.setProcessStartDate(checksumInstant); + mrc.setProcessEndDate(checksumInstant); + checksumInstant = checksumInstant.plusSeconds(10); + } + context.commit(); + } + + @After + public void cleanUp() throws SQLException { + // Need to clean up ChecksumHistory because of a referential integrity + // constraint violation between the most_recent_checksum table and + // bitstream tables + ChecksumHistoryService checksumHistoryService = CheckerServiceFactory.getInstance().getChecksumHistoryService(); + + for (Bitstream bitstream: bitstreams) { + checksumHistoryService.deleteByBitstream(context, bitstream); + } + } + + @Test + public void testChecksumsRecordedWhenProcesingIsInterrupted() throws SQLException { + CheckerCommand checker = new CheckerCommand(context); + + // The start date to use for the checker process + Instant checkerStartDate = Instant.now(); + + // Verify that all checksums are before the checker start date + for (Bitstream bitstream: bitstreams) { + MostRecentChecksum checksum = checksumService.findByBitstream(context, bitstream); + Instant lastChecksumDate = checksum.getProcessStartDate(); + assertTrue("lastChecksumDate (" + lastChecksumDate + ") <= checkerStartDate (" + checkerStartDate + ")", + lastChecksumDate.isBefore(checkerStartDate)); + } + + // Dispatcher that throws an exception when a third bitstream is + // retrieved. + BitstreamDispatcher dispatcher = new ExpectionThrowingDispatcher( + context, checkerStartDate, false, 2); + checker.setDispatcher(dispatcher); + + + // Run the checksum checker + checker.setProcessStartDate(checkerStartDate); + try { + checker.process(); + fail("SQLException should have been thrown"); + } catch (SQLException sqle) { + // Rollback any pending transaction + context.rollback(); + } + + // Verify that the checksums of the first two bitstreams (that were + // processed before the exception) have been successfully recorded in + // the database, while the third bitstream was not updated. + int bitstreamCount = 0; + for (Bitstream bitstream: bitstreams) { + MostRecentChecksum checksum = checksumService.findByBitstream(context, bitstream); + Instant lastChecksumDate = checksum.getProcessStartDate(); + + bitstreamCount = bitstreamCount + 1; + if (bitstreamCount <= 2) { + assertTrue("lastChecksumDate (" + lastChecksumDate + ") <= checkerStartDate (" + checkerStartDate + ")", + lastChecksumDate.isAfter(checkerStartDate)); + } else { + assertTrue("lastChecksumDate (" + lastChecksumDate + ") >= checkerStartDate (" + checkerStartDate + ")", + lastChecksumDate.isBefore(checkerStartDate)); + } + } + } + + /** + * Subclass of SimpleDispatcher that only allows a limited number of "next" + * class before throwing a SQLException. + */ + class ExpectionThrowingDispatcher extends SimpleDispatcher { + // The number of "next" calls to allow before throwing a SQLException + protected int maxNextCalls; + + // The number of "next" method calls seen so far. + protected int numNextCalls = 0; + + /** + * Constructor. + * + * @param context Context + * @param startTime timestamp for beginning of checker process + * @param looping indicates whether checker should loop infinitely + * through most_recent_checksum table + * @param maxNextCalls the number of "next" method calls to allow before + * throwing a SQLException. + */ + public ExpectionThrowingDispatcher(Context context, Instant startTime, boolean looping, int maxNextCalls) { + super(context, startTime, looping); + this.maxNextCalls = maxNextCalls; + } + + /** + * Selects the next candidate bitstream. + * + * After "maxNextClass" number of calls, this method throws a + * SQLException. + * + * @throws SQLException if database error + */ + @Override + public synchronized Bitstream next() throws SQLException { + numNextCalls = numNextCalls + 1; + if (numNextCalls > maxNextCalls) { + throw new SQLException("Max 'next' method calls exceeded"); + } + return super.next(); + } + } +} From ecdab82627310061d97f67a6d9546d80359d15b7 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 15:09:41 -0500 Subject: [PATCH 253/701] Revert to using java.util.Date instead of Instant to support earlier versions of DSpace. --- .../org/dspace/checker/ChecksumCheckerIT.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java index 7653d518b196..34198ff1ebfe 100644 --- a/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java +++ b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java @@ -14,6 +14,7 @@ import java.sql.SQLException; import java.time.Instant; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.apache.commons.io.IOUtils; @@ -76,8 +77,8 @@ public void setup() throws Exception { Instant checksumInstant = Instant.ofEpochMilli(0); for (Bitstream bitstream: bitstreams) { MostRecentChecksum mrc = checksumService.findByBitstream(context, bitstream); - mrc.setProcessStartDate(checksumInstant); - mrc.setProcessEndDate(checksumInstant); + mrc.setProcessStartDate(Date.from(checksumInstant)); + mrc.setProcessEndDate(Date.from(checksumInstant)); checksumInstant = checksumInstant.plusSeconds(10); } context.commit(); @@ -100,14 +101,14 @@ public void testChecksumsRecordedWhenProcesingIsInterrupted() throws SQLExceptio CheckerCommand checker = new CheckerCommand(context); // The start date to use for the checker process - Instant checkerStartDate = Instant.now(); + Date checkerStartDate = Date.from(Instant.now()); // Verify that all checksums are before the checker start date for (Bitstream bitstream: bitstreams) { MostRecentChecksum checksum = checksumService.findByBitstream(context, bitstream); - Instant lastChecksumDate = checksum.getProcessStartDate(); + Date lastChecksumDate = checksum.getProcessStartDate(); assertTrue("lastChecksumDate (" + lastChecksumDate + ") <= checkerStartDate (" + checkerStartDate + ")", - lastChecksumDate.isBefore(checkerStartDate)); + lastChecksumDate.before(checkerStartDate)); } // Dispatcher that throws an exception when a third bitstream is @@ -133,15 +134,15 @@ public void testChecksumsRecordedWhenProcesingIsInterrupted() throws SQLExceptio int bitstreamCount = 0; for (Bitstream bitstream: bitstreams) { MostRecentChecksum checksum = checksumService.findByBitstream(context, bitstream); - Instant lastChecksumDate = checksum.getProcessStartDate(); + Date lastChecksumDate = checksum.getProcessStartDate(); bitstreamCount = bitstreamCount + 1; if (bitstreamCount <= 2) { assertTrue("lastChecksumDate (" + lastChecksumDate + ") <= checkerStartDate (" + checkerStartDate + ")", - lastChecksumDate.isAfter(checkerStartDate)); + lastChecksumDate.after(checkerStartDate)); } else { assertTrue("lastChecksumDate (" + lastChecksumDate + ") >= checkerStartDate (" + checkerStartDate + ")", - lastChecksumDate.isBefore(checkerStartDate)); + lastChecksumDate.before(checkerStartDate)); } } } @@ -167,7 +168,7 @@ class ExpectionThrowingDispatcher extends SimpleDispatcher { * @param maxNextCalls the number of "next" method calls to allow before * throwing a SQLException. */ - public ExpectionThrowingDispatcher(Context context, Instant startTime, boolean looping, int maxNextCalls) { + public ExpectionThrowingDispatcher(Context context, Date startTime, boolean looping, int maxNextCalls) { super(context, startTime, looping); this.maxNextCalls = maxNextCalls; } From bce865f299d69f46cc1a84938b4feb73577bdd61 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 15:09:41 -0500 Subject: [PATCH 254/701] Revert to using java.util.Date instead of Instant to support earlier versions of DSpace. --- .../org/dspace/checker/ChecksumCheckerIT.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java index 7653d518b196..34198ff1ebfe 100644 --- a/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java +++ b/dspace-api/src/test/java/org/dspace/checker/ChecksumCheckerIT.java @@ -14,6 +14,7 @@ import java.sql.SQLException; import java.time.Instant; import java.util.ArrayList; +import java.util.Date; import java.util.List; import org.apache.commons.io.IOUtils; @@ -76,8 +77,8 @@ public void setup() throws Exception { Instant checksumInstant = Instant.ofEpochMilli(0); for (Bitstream bitstream: bitstreams) { MostRecentChecksum mrc = checksumService.findByBitstream(context, bitstream); - mrc.setProcessStartDate(checksumInstant); - mrc.setProcessEndDate(checksumInstant); + mrc.setProcessStartDate(Date.from(checksumInstant)); + mrc.setProcessEndDate(Date.from(checksumInstant)); checksumInstant = checksumInstant.plusSeconds(10); } context.commit(); @@ -100,14 +101,14 @@ public void testChecksumsRecordedWhenProcesingIsInterrupted() throws SQLExceptio CheckerCommand checker = new CheckerCommand(context); // The start date to use for the checker process - Instant checkerStartDate = Instant.now(); + Date checkerStartDate = Date.from(Instant.now()); // Verify that all checksums are before the checker start date for (Bitstream bitstream: bitstreams) { MostRecentChecksum checksum = checksumService.findByBitstream(context, bitstream); - Instant lastChecksumDate = checksum.getProcessStartDate(); + Date lastChecksumDate = checksum.getProcessStartDate(); assertTrue("lastChecksumDate (" + lastChecksumDate + ") <= checkerStartDate (" + checkerStartDate + ")", - lastChecksumDate.isBefore(checkerStartDate)); + lastChecksumDate.before(checkerStartDate)); } // Dispatcher that throws an exception when a third bitstream is @@ -133,15 +134,15 @@ public void testChecksumsRecordedWhenProcesingIsInterrupted() throws SQLExceptio int bitstreamCount = 0; for (Bitstream bitstream: bitstreams) { MostRecentChecksum checksum = checksumService.findByBitstream(context, bitstream); - Instant lastChecksumDate = checksum.getProcessStartDate(); + Date lastChecksumDate = checksum.getProcessStartDate(); bitstreamCount = bitstreamCount + 1; if (bitstreamCount <= 2) { assertTrue("lastChecksumDate (" + lastChecksumDate + ") <= checkerStartDate (" + checkerStartDate + ")", - lastChecksumDate.isAfter(checkerStartDate)); + lastChecksumDate.after(checkerStartDate)); } else { assertTrue("lastChecksumDate (" + lastChecksumDate + ") >= checkerStartDate (" + checkerStartDate + ")", - lastChecksumDate.isBefore(checkerStartDate)); + lastChecksumDate.before(checkerStartDate)); } } } @@ -167,7 +168,7 @@ class ExpectionThrowingDispatcher extends SimpleDispatcher { * @param maxNextCalls the number of "next" method calls to allow before * throwing a SQLException. */ - public ExpectionThrowingDispatcher(Context context, Instant startTime, boolean looping, int maxNextCalls) { + public ExpectionThrowingDispatcher(Context context, Date startTime, boolean looping, int maxNextCalls) { super(context, startTime, looping); this.maxNextCalls = maxNextCalls; } From aa0fe083a60fb4a77485bd2e6416ef20d2cb5385 Mon Sep 17 00:00:00 2001 From: im-shubham-vish Date: Tue, 12 Aug 2025 16:55:53 +0530 Subject: [PATCH 255/701] Make parse protected to make it override in MetadataExportSearch Add Test for Double Quoted Search CSV Export (cherry picked from commit 39a45f7f3465df36fbf8db3f5a9ed541eab794d8) --- .../app/bulkedit/MetadataExportSearch.java | 12 +++++++ .../org/dspace/scripts/DSpaceRunnable.java | 2 +- .../app/bulkedit/MetadataExportSearchIT.java | 31 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java index e4bbe335d63e..689df4701a96 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.UUID; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.DefaultParser.Builder; import org.apache.commons.cli.ParseException; import org.dspace.content.Item; import org.dspace.content.MetadataDSpaceCsvExportServiceImpl; @@ -167,4 +169,14 @@ public IndexableObject resolveScope(Context context, String id) throws SQLExcept } return scopeObj; } + + @Override + protected StepResult parse(String[] args) throws ParseException { + commandLine = new DefaultParser().parse(getScriptConfiguration().getOptions(), args); + Builder builder = new DefaultParser().builder(); + builder.setStripLeadingAndTrailingQuotes(false); + commandLine = builder.build().parse(getScriptConfiguration().getOptions(), args); + setup(); + return StepResult.Continue; + } } diff --git a/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java b/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java index 2ea0a52d6e34..8f905e01511e 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java +++ b/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java @@ -117,7 +117,7 @@ private void handleHelpCommandLine() { * @param args The primitive array of Strings representing the parameters * @throws ParseException If something goes wrong */ - private StepResult parse(String[] args) throws ParseException { + protected StepResult parse(String[] args) throws ParseException { commandLine = new DefaultParser().parse(getScriptConfiguration().getOptions(), args); setup(); return StepResult.Continue; diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java index 63a87a48f554..15a6371e920b 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java @@ -251,4 +251,35 @@ public void exportMetadataSearchNonExistinFacetsTest() throws Exception { assertNotNull(exception); assertEquals("nonExisting is not a valid search filter", exception.getMessage()); } + + @Test + public void exportMetadataSearchDoubleQuotedArgumentTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item quotedItem1 = ItemBuilder.createItem(context, collection) + .withTitle("The Special Runnable Item") + .withSubject("quoted-subject") + .build(); + Item quotedItem2 = ItemBuilder.createItem(context, collection) + .withTitle("The Special Item") + .withSubject("quoted-subject") + .build(); + context.restoreAuthSystemState(); + + int result = runDSpaceScript( + "metadata-export-search", + "-q", "title:\"Special Runnable\"", + "-n", filename); + + assertEquals(0, result); + + Item[] expectedResult = new Item[] {quotedItem1}; + checkItemsPresentInFile(filename, expectedResult); + + File file = new File(filename); + try (Reader reader = Files.newReader(file, Charset.defaultCharset()); + CSVReader csvReader = new CSVReader(reader)) { + List lines = csvReader.readAll(); + assertEquals("Unexpected extra items in export", 2, lines.size()); + } + } } From f79d012cbc1b2ad59cb11814d50219e1d6c91458 Mon Sep 17 00:00:00 2001 From: im-shubham-vish Date: Tue, 12 Aug 2025 16:55:53 +0530 Subject: [PATCH 256/701] Make parse protected to make it override in MetadataExportSearch Add Test for Double Quoted Search CSV Export (cherry picked from commit 39a45f7f3465df36fbf8db3f5a9ed541eab794d8) --- .../app/bulkedit/MetadataExportSearch.java | 12 +++++++ .../org/dspace/scripts/DSpaceRunnable.java | 2 +- .../app/bulkedit/MetadataExportSearchIT.java | 31 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java index e4bbe335d63e..689df4701a96 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearch.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.UUID; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.DefaultParser.Builder; import org.apache.commons.cli.ParseException; import org.dspace.content.Item; import org.dspace.content.MetadataDSpaceCsvExportServiceImpl; @@ -167,4 +169,14 @@ public IndexableObject resolveScope(Context context, String id) throws SQLExcept } return scopeObj; } + + @Override + protected StepResult parse(String[] args) throws ParseException { + commandLine = new DefaultParser().parse(getScriptConfiguration().getOptions(), args); + Builder builder = new DefaultParser().builder(); + builder.setStripLeadingAndTrailingQuotes(false); + commandLine = builder.build().parse(getScriptConfiguration().getOptions(), args); + setup(); + return StepResult.Continue; + } } diff --git a/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java b/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java index 2ea0a52d6e34..8f905e01511e 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java +++ b/dspace-api/src/main/java/org/dspace/scripts/DSpaceRunnable.java @@ -117,7 +117,7 @@ private void handleHelpCommandLine() { * @param args The primitive array of Strings representing the parameters * @throws ParseException If something goes wrong */ - private StepResult parse(String[] args) throws ParseException { + protected StepResult parse(String[] args) throws ParseException { commandLine = new DefaultParser().parse(getScriptConfiguration().getOptions(), args); setup(); return StepResult.Continue; diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java index 63a87a48f554..15a6371e920b 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataExportSearchIT.java @@ -251,4 +251,35 @@ public void exportMetadataSearchNonExistinFacetsTest() throws Exception { assertNotNull(exception); assertEquals("nonExisting is not a valid search filter", exception.getMessage()); } + + @Test + public void exportMetadataSearchDoubleQuotedArgumentTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item quotedItem1 = ItemBuilder.createItem(context, collection) + .withTitle("The Special Runnable Item") + .withSubject("quoted-subject") + .build(); + Item quotedItem2 = ItemBuilder.createItem(context, collection) + .withTitle("The Special Item") + .withSubject("quoted-subject") + .build(); + context.restoreAuthSystemState(); + + int result = runDSpaceScript( + "metadata-export-search", + "-q", "title:\"Special Runnable\"", + "-n", filename); + + assertEquals(0, result); + + Item[] expectedResult = new Item[] {quotedItem1}; + checkItemsPresentInFile(filename, expectedResult); + + File file = new File(filename); + try (Reader reader = Files.newReader(file, Charset.defaultCharset()); + CSVReader csvReader = new CSVReader(reader)) { + List lines = csvReader.readAll(); + assertEquals("Unexpected extra items in export", 2, lines.size()); + } + } } From fc1bd872b016695a2e60dfa554c55f206f9f359f Mon Sep 17 00:00:00 2001 From: April Herron Date: Mon, 13 Aug 2018 14:18:17 -0400 Subject: [PATCH 257/701] Fix updateMissingBitstreams to use single database operation instead of o-o operations [DS-3975] Includes toniprieto's suggested change in #2169 discussion and additional logging (e.g. how many bitstreams are missing). --- .../MostRecentChecksumServiceImpl.java | 55 ++----------------- .../checker/dao/MostRecentChecksumDAO.java | 2 + .../dao/impl/MostRecentChecksumDAOImpl.java | 18 ++++++ 3 files changed, 25 insertions(+), 50 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/checker/MostRecentChecksumServiceImpl.java b/dspace-api/src/main/java/org/dspace/checker/MostRecentChecksumServiceImpl.java index d267171aa0d9..9ee777a3e15b 100644 --- a/dspace-api/src/main/java/org/dspace/checker/MostRecentChecksumServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/checker/MostRecentChecksumServiceImpl.java @@ -90,61 +90,16 @@ public List findBitstreamResultTypeReport(Context context, D /** * Queries the bitstream table for bitstream IDs that are not yet in the * most_recent_checksum table, and inserts them into the - * most_recent_checksum and checksum_history tables. - * + * most_recent_checksum table. * @param context Context * @throws SQLException if database error */ @Override public void updateMissingBitstreams(Context context) throws SQLException { -// "insert into most_recent_checksum ( " -// + "bitstream_id, to_be_processed, expected_checksum, current_checksum, " -// + "last_process_start_date, last_process_end_date, " -// + "checksum_algorithm, matched_prev_checksum, result ) " -// + "select bitstream.bitstream_id, " -// + "CASE WHEN bitstream.deleted = false THEN true ELSE false END, " -// + "CASE WHEN bitstream.checksum IS NULL THEN '' ELSE bitstream.checksum END, " -// + "CASE WHEN bitstream.checksum IS NULL THEN '' ELSE bitstream.checksum END, " -// + "?, ?, CASE WHEN bitstream.checksum_algorithm IS NULL " -// + "THEN 'MD5' ELSE bitstream.checksum_algorithm END, true, " -// + "CASE WHEN bitstream.deleted = true THEN 'BITSTREAM_MARKED_DELETED' else 'CHECKSUM_MATCH' END " -// + "from bitstream where not exists( " -// + "select 'x' from most_recent_checksum " -// + "where most_recent_checksum.bitstream_id = bitstream.bitstream_id )"; - - List unknownBitstreams = bitstreamService.findBitstreamsWithNoRecentChecksum(context); - for (Bitstream bitstream : unknownBitstreams) { - log.info(bitstream + " " + bitstream.getID().toString() + " " + bitstream.getName()); - - MostRecentChecksum mostRecentChecksum = new MostRecentChecksum(); - mostRecentChecksum.setBitstream(bitstream); - //Only process if our bitstream isn't deleted - mostRecentChecksum.setToBeProcessed(!bitstream.isDeleted()); - if (bitstream.getChecksum() == null) { - mostRecentChecksum.setCurrentChecksum(""); - mostRecentChecksum.setExpectedChecksum(""); - } else { - mostRecentChecksum.setCurrentChecksum(bitstream.getChecksum()); - mostRecentChecksum.setExpectedChecksum(bitstream.getChecksum()); - } - mostRecentChecksum.setProcessStartDate(new Date()); - mostRecentChecksum.setProcessEndDate(new Date()); - if (bitstream.getChecksumAlgorithm() == null) { - mostRecentChecksum.setChecksumAlgorithm("MD5"); - } else { - mostRecentChecksum.setChecksumAlgorithm(bitstream.getChecksumAlgorithm()); - } - mostRecentChecksum.setMatchedPrevChecksum(true); - ChecksumResult checksumResult; - if (bitstream.isDeleted()) { - checksumResult = checksumResultService.findByCode(context, ChecksumResultCode.BITSTREAM_MARKED_DELETED); - } else { - checksumResult = checksumResultService.findByCode(context, ChecksumResultCode.CHECKSUM_MATCH); - } - mostRecentChecksum.setChecksumResult(checksumResult); - mostRecentChecksumDAO.create(context, mostRecentChecksum); - mostRecentChecksumDAO.save(context, mostRecentChecksum); - } + log.info("Retrieving missing bitsreams (bitstream IDs that are not yet in most_recent_checksum table)..."); + int updated = mostRecentChecksumDAO.updateMissingBitstreams(context); + log.info("Updated most_recent_checksum for " + updated + " bitstreams."); + log.info("Missing bitsreams processing done."); } @Override diff --git a/dspace-api/src/main/java/org/dspace/checker/dao/MostRecentChecksumDAO.java b/dspace-api/src/main/java/org/dspace/checker/dao/MostRecentChecksumDAO.java index 56485c9b4b4b..73a81e4d9b4f 100644 --- a/dspace-api/src/main/java/org/dspace/checker/dao/MostRecentChecksumDAO.java +++ b/dspace-api/src/main/java/org/dspace/checker/dao/MostRecentChecksumDAO.java @@ -33,6 +33,8 @@ public List findByNotProcessedInDateRange(Context context, D public List findByResultTypeInDateRange(Context context, Date startDate, Date endDate, ChecksumResultCode resultCode) throws SQLException; + public int updateMissingBitstreams(Context context) throws SQLException; + public void deleteByBitstream(Context context, Bitstream bitstream) throws SQLException; public MostRecentChecksum getOldestRecord(Context context) throws SQLException; diff --git a/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java b/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java index 669621aeeb58..c61ead6adf76 100644 --- a/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java @@ -66,6 +66,24 @@ public List findByNotProcessedInDateRange(Context context, D return list(context, criteriaQuery, false, MostRecentChecksum.class, -1, -1); } + @Override + public int updateMissingBitstreams(Context context) throws SQLException { + String hql = "INSERT INTO MostRecentChecksum(bitstream, toBeProcessed, expectedChecksum, currentChecksum, " + + "processStartDate, processEndDate, checksumAlgorithm, matchedPrevChecksum, checksumResult) " + + "SELECT b, " + + "CASE WHEN deleted = false THEN true ELSE false END, " + + "CASE WHEN checksum IS NULL THEN '' ELSE checksum END, " + + "CASE WHEN checksum IS NULL THEN '' ELSE checksum END, " + + "current_date(), current_date(), " + + "CASE WHEN checksumAlgorithm IS NULL THEN 'MD5' ELSE checksumAlgorithm END, " + + "CAST(1 AS boolean), " + + "(SELECT cr FROM ChecksumResult AS cr WHERE " + + "(resultCode='BITSTREAM_MARKED_DELETED' AND b.deleted = true) " + + "OR (resultCode='CHECKSUM_MATCH' AND b.deleted = false)) " + + "FROM Bitstream AS b WHERE NOT EXISTS(SELECT 'x' FROM MostRecentChecksum WHERE id=b.id)"; + Query query = createQuery(context, hql); + return query.executeUpdate(); + } @Override public MostRecentChecksum findByBitstream(Context context, Bitstream bitstream) throws SQLException { From c358e885ae6833ab8bf8d8a28789ef31443c4545 Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Fri, 15 Aug 2025 19:34:50 +0300 Subject: [PATCH 258/701] Fix types used in updateMissingBitstreams HQL Error found when running checker ITs in #11144. --- .../checker/dao/impl/MostRecentChecksumDAOImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java b/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java index c61ead6adf76..b68a456d58cb 100644 --- a/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/checker/dao/impl/MostRecentChecksumDAOImpl.java @@ -74,13 +74,13 @@ public int updateMissingBitstreams(Context context) throws SQLException { "CASE WHEN deleted = false THEN true ELSE false END, " + "CASE WHEN checksum IS NULL THEN '' ELSE checksum END, " + "CASE WHEN checksum IS NULL THEN '' ELSE checksum END, " + - "current_date(), current_date(), " + + "current_timestamp(), current_timestamp(), " + "CASE WHEN checksumAlgorithm IS NULL THEN 'MD5' ELSE checksumAlgorithm END, " + "CAST(1 AS boolean), " + "(SELECT cr FROM ChecksumResult AS cr WHERE " + - "(resultCode='BITSTREAM_MARKED_DELETED' AND b.deleted = true) " + - "OR (resultCode='CHECKSUM_MATCH' AND b.deleted = false)) " + - "FROM Bitstream AS b WHERE NOT EXISTS(SELECT 'x' FROM MostRecentChecksum WHERE id=b.id)"; + "(resultCode = 'BITSTREAM_MARKED_DELETED' AND b.deleted = true) " + + "OR (resultCode = 'CHECKSUM_MATCH' AND b.deleted = false)) " + + "FROM Bitstream AS b WHERE NOT EXISTS(SELECT 'x' FROM MostRecentChecksum AS c WHERE c.bitstream = b)"; Query query = createQuery(context, hql); return query.executeUpdate(); } From d282f92427f340a7f8ca68ca8316114ce0bdd524 Mon Sep 17 00:00:00 2001 From: Martin Walk Date: Wed, 20 Aug 2025 15:45:14 +0200 Subject: [PATCH 259/701] Fix #11074 export simple archive format with no collection --- .../app/itemexport/ItemExportServiceImpl.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java index 9eaabc20e862..d50b44fd8d4c 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java @@ -352,7 +352,7 @@ protected void writeHandle(Context c, Item i, File destDir) /** * Create the 'collections' file. List handles of all Collections which - * contain this Item. The "owning" Collection is listed first. + * contain this Item. The "owning" Collection is listed first. * * @param item list collections holding this Item. * @param destDir write the file here. @@ -363,12 +363,14 @@ protected void writeCollections(Item item, File destDir) File outFile = new File(destDir, "collections"); if (outFile.createNewFile()) { try (PrintWriter out = new PrintWriter(new FileWriter(outFile))) { - String ownerHandle = item.getOwningCollection().getHandle(); - out.println(ownerHandle); + Collection owningCollection = item.getOwningCollection(); + // The owning collection is null for workspace and workflow items + if (owningCollection != null) { + out.println(owningCollection.getHandle()); + } for (Collection collection : item.getCollections()) { - String collectionHandle = collection.getHandle(); - if (!collectionHandle.equals(ownerHandle)) { - out.println(collectionHandle); + if (!collection.equals(owningCollection)) { + out.println(collection.getHandle()); } } } From d063f1ef66ee853f23d2a286fecac7f9cbecb077 Mon Sep 17 00:00:00 2001 From: Joran De Braekeleer Date: Wed, 13 Aug 2025 13:32:33 +0200 Subject: [PATCH 260/701] 133268: Metadata import empty collection col check (cherry picked from commit b9513228d78ccb3aa8bcb8a94e2d0a8e1a37c30b) --- .../src/main/java/org/dspace/app/bulkedit/MetadataImport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java index ad46cb95c353..7873f1b12ca8 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java @@ -494,7 +494,7 @@ public List runImport(Context c, boolean change, // Check it has an owning collection List collections = line.get("collection"); - if (collections == null) { + if (collections == null || collections.isEmpty()) { throw new MetadataImportException( "New items must have a 'collection' assigned in the form of a handle"); } From d44a5ddc499102825cf44800e1f7af940f0d318c Mon Sep 17 00:00:00 2001 From: Joran De Braekeleer Date: Wed, 13 Aug 2025 13:32:33 +0200 Subject: [PATCH 261/701] 133268: Metadata import empty collection col check (cherry picked from commit b9513228d78ccb3aa8bcb8a94e2d0a8e1a37c30b) --- .../src/main/java/org/dspace/app/bulkedit/MetadataImport.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java index e8cf42b47c1b..b12abbc46eb9 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java @@ -494,7 +494,7 @@ public List runImport(Context c, boolean change, // Check it has an owning collection List collections = line.get("collection"); - if (collections == null) { + if (collections == null || collections.isEmpty()) { throw new MetadataImportException( "New items must have a 'collection' assigned in the form of a handle"); } From 3a160624b73e37fafd8e77a65cd41c38207b0b75 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Sun, 17 Aug 2025 11:56:13 +0300 Subject: [PATCH 262/701] .github/workflows/codescan.yml: use codeql-action v3 Version 2 was deprecated in January, 2024 after the release of v3. See: https://github.blog/changelog/2025-01-10-code-scanning-codeql-action-v2-is-now-deprecated/ (cherry picked from commit 19f3535dfdd57c46ad2b8a704a11fabd681fceb8) --- .github/workflows/codescan.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index 3a563c6fa39c..cbdb4b880cbb 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. # https://github.com/github/codeql-action - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: # Codescan Javascript as well since a few JS files exist in REST API's interface languages: java, javascript @@ -56,8 +56,8 @@ jobs: # NOTE: Based on testing, this autobuild process works well for DSpace. A custom # DSpace build w/caching (like in build.yml) was about the same speed as autobuild. - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # Perform GitHub Code Scanning. - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 94a1a0b2e0eaea14f87ed8bd2c64187f9df6e2df Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Sun, 17 Aug 2025 11:56:13 +0300 Subject: [PATCH 263/701] .github/workflows/codescan.yml: use codeql-action v3 Version 2 was deprecated in January, 2024 after the release of v3. See: https://github.blog/changelog/2025-01-10-code-scanning-codeql-action-v2-is-now-deprecated/ --- .github/workflows/codescan.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index 1e3d835e2713..88931d478e6f 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. # https://github.com/github/codeql-action - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: # Codescan Javascript as well since a few JS files exist in REST API's interface languages: java, javascript @@ -56,8 +56,8 @@ jobs: # NOTE: Based on testing, this autobuild process works well for DSpace. A custom # DSpace build w/caching (like in build.yml) was about the same speed as autobuild. - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # Perform GitHub Code Scanning. - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From dc768cd5ae059a71b882daeace6343635cf163c4 Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Tue, 12 Aug 2025 19:57:57 +0300 Subject: [PATCH 264/701] Fix bean initialization error on Tomcat startup if citation-page.enabled_communities is set Arrays.asList produces non-mutable list but with communities, sub-collections are added dynamically on initializaion. --- .../org/dspace/disseminate/CitationDocumentServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java index c20961db7544..b1441867772f 100644 --- a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java @@ -139,8 +139,8 @@ public void afterPropertiesSet() throws Exception { //Load enabled collections String[] citationEnabledCollections = configurationService - .getArrayProperty("citation-page.enabled_collections"); - citationEnabledCollectionsList = Arrays.asList(citationEnabledCollections); + .getArrayProperty("citation-page.enabled_collections"); + citationEnabledCollectionsList = new ArrayList(Arrays.asList(citationEnabledCollections)); //Load enabled communities, and add to collection-list String[] citationEnabledCommunities = configurationService From e51a2537aca68982cfeea39c1a753e479b561711 Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Tue, 12 Aug 2025 19:57:57 +0300 Subject: [PATCH 265/701] Fix bean initialization error on Tomcat startup if citation-page.enabled_communities is set Arrays.asList produces non-mutable list but with communities, sub-collections are added dynamically on initializaion. --- .../org/dspace/disseminate/CitationDocumentServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java index c20961db7544..b1441867772f 100644 --- a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java @@ -139,8 +139,8 @@ public void afterPropertiesSet() throws Exception { //Load enabled collections String[] citationEnabledCollections = configurationService - .getArrayProperty("citation-page.enabled_collections"); - citationEnabledCollectionsList = Arrays.asList(citationEnabledCollections); + .getArrayProperty("citation-page.enabled_collections"); + citationEnabledCollectionsList = new ArrayList(Arrays.asList(citationEnabledCollections)); //Load enabled communities, and add to collection-list String[] citationEnabledCommunities = configurationService From 922109b10ace76c6c7d956ac21edb0c01d46f5df Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Fri, 15 Aug 2025 09:57:51 +0300 Subject: [PATCH 266/701] dspace-api/pom.xml: remove build-helper-maven-plugin Remove the org.codehaus.mojo:build-helper-maven-plugin because the `maven-version` property has been included by maven itself since version 3.0.4. This fixes the following warning during build: [INFO] --- build-helper:3.6.1:maven-version (default) @ dspace-api --- [WARNING] Goal 'maven-version' is deprecated: Maven since version 3.0.4 has such property build in: MNG-4112 . So goal can be removed. (cherry picked from commit de3170d4ebbcc2168e639d681f0433b9dec293b5) --- dspace-api/pom.xml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6f084d235c73..3488fda26bf8 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -99,20 +99,6 @@ - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.1 - - - validate - - maven-version - - - - - org.codehaus.mojo buildnumber-maven-plugin From d2f6140455de5a807c4ad8f6bbb5bdf7cf9f9a0c Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Fri, 15 Aug 2025 09:57:51 +0300 Subject: [PATCH 267/701] dspace-api/pom.xml: remove build-helper-maven-plugin Remove the org.codehaus.mojo:build-helper-maven-plugin because the `maven-version` property has been included by maven itself since version 3.0.4. This fixes the following warning during build: [INFO] --- build-helper:3.6.1:maven-version (default) @ dspace-api --- [WARNING] Goal 'maven-version' is deprecated: Maven since version 3.0.4 has such property build in: MNG-4112 . So goal can be removed. (cherry picked from commit de3170d4ebbcc2168e639d681f0433b9dec293b5) --- dspace-api/pom.xml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d0857bf4eed1..39944a08b593 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -99,20 +99,6 @@ - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.1 - - - validate - - maven-version - - - - - org.codehaus.mojo buildnumber-maven-plugin From f831acf4d703ee13f0703ed5b2ed237b09efe991 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:05:45 -0300 Subject: [PATCH 268/701] fix(#8852): Statistics event processor now uses oaiPrefix instead of getHost (cherry picked from commit c8fe80c22bdc67d8b4eae0575fb8a2cab31d2010) --- .../statistics/export/processor/ExportEventProcessor.java | 3 ++- .../statistics/export/ITIrusExportUsageEventListener.java | 1 + .../statistics/export/processor/ExportEventProcessorIT.java | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java b/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java index 609298779d34..562b8547ce9d 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java +++ b/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java @@ -136,9 +136,10 @@ protected String getBaseParameters(Item item) .append(URLEncoder.encode(clientUA, UTF_8)); String hostName = Utils.getHostName(configurationService.getProperty("dspace.ui.url")); + String oaiPrefix = configurationService.getProperty("oai.identifier.prefix"); data.append("&").append(URLEncoder.encode("rft.artnum", UTF_8)).append("="). - append(URLEncoder.encode("oai:" + hostName + ":" + item + append(URLEncoder.encode("oai:" + oaiPrefix + ":" + item .getHandle(), UTF_8)); data.append("&").append(URLEncoder.encode("rfr_dat", UTF_8)).append("=") .append(URLEncoder.encode(referer, UTF_8)); diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java b/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java index e28e8284a218..77a51a92f352 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java +++ b/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java @@ -116,6 +116,7 @@ public void setUp() throws Exception { configurationService.setProperty("irus.statistics.tracker.enabled", true); configurationService.setProperty("irus.statistics.tracker.type-field", "dc.type"); configurationService.setProperty("irus.statistics.tracker.type-value", "Excluded type"); + configurationService.setProperty("oai.identifier.prefix", "localhost"); context.turnOffAuthorisationSystem(); diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java index e42003e4fc8b..1ae7d2e4905f 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java +++ b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java @@ -62,6 +62,7 @@ public void setUp() throws Exception { configurationService.setProperty("irus.statistics.tracker.enabled", true); configurationService.setProperty("irus.statistics.tracker.type-field", "dc.type"); configurationService.setProperty("irus.statistics.tracker.type-value", "Excluded type"); + configurationService.setProperty("oai.identifier.prefix", "localhost"); context.turnOffAuthorisationSystem(); publication = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build(); From 8a326fd327120bcf63cba1a2c7c17f744bb4ddb6 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:05:45 -0300 Subject: [PATCH 269/701] fix(#8852): Statistics event processor now uses oaiPrefix instead of getHost (cherry picked from commit c8fe80c22bdc67d8b4eae0575fb8a2cab31d2010) --- .../statistics/export/processor/ExportEventProcessor.java | 3 ++- .../statistics/export/ITIrusExportUsageEventListener.java | 1 + .../statistics/export/processor/ExportEventProcessorIT.java | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java b/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java index 434de459bad9..44fc5f3dc9c5 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java +++ b/dspace-api/src/main/java/org/dspace/statistics/export/processor/ExportEventProcessor.java @@ -136,9 +136,10 @@ protected String getBaseParameters(Item item) .append(URLEncoder.encode(clientUA, UTF_8)); String hostName = Utils.getHostName(configurationService.getProperty("dspace.ui.url")); + String oaiPrefix = configurationService.getProperty("oai.identifier.prefix"); data.append("&").append(URLEncoder.encode("rft.artnum", UTF_8)).append("="). - append(URLEncoder.encode("oai:" + hostName + ":" + item + append(URLEncoder.encode("oai:" + oaiPrefix + ":" + item .getHandle(), UTF_8)); data.append("&").append(URLEncoder.encode("rfr_dat", UTF_8)).append("=") .append(URLEncoder.encode(referer, UTF_8)); diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java b/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java index 0c861a0d293d..48cf0b14b6bd 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java +++ b/dspace-api/src/test/java/org/dspace/statistics/export/ITIrusExportUsageEventListener.java @@ -116,6 +116,7 @@ public void setUp() throws Exception { configurationService.setProperty("irus.statistics.tracker.enabled", true); configurationService.setProperty("irus.statistics.tracker.type-field", "dc.type"); configurationService.setProperty("irus.statistics.tracker.type-value", "Excluded type"); + configurationService.setProperty("oai.identifier.prefix", "localhost"); context.turnOffAuthorisationSystem(); diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java index fb53d0c83c54..96909a9e3dbd 100644 --- a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java +++ b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java @@ -62,6 +62,7 @@ public void setUp() throws Exception { configurationService.setProperty("irus.statistics.tracker.enabled", true); configurationService.setProperty("irus.statistics.tracker.type-field", "dc.type"); configurationService.setProperty("irus.statistics.tracker.type-value", "Excluded type"); + configurationService.setProperty("oai.identifier.prefix", "localhost"); context.turnOffAuthorisationSystem(); publication = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build(); From b6bacd01ac4098d96063e537c1ec603f0e7a5f92 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:43:19 +0000 Subject: [PATCH 270/701] Bump the build-tools group with 3 updates Bumps the build-tools group with 3 updates: [com.github.spotbugs:spotbugs](https://github.com/spotbugs/spotbugs), [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) and [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin). Updates `com.github.spotbugs:spotbugs` from 4.9.3 to 4.9.4 - [Release notes](https://github.com/spotbugs/spotbugs/releases) - [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotbugs/spotbugs/compare/4.9.3...4.9.4) Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.9.3.2 to 4.9.4.0 - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.3.2...spotbugs-maven-plugin-4.9.4.0) Updates `org.apache.maven.plugins:maven-javadoc-plugin` from 3.11.2 to 3.11.3 - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.11.2...maven-javadoc-plugin-3.11.3) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs dependency-version: 4.9.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.4.0 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-version: 3.11.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index ab300551b21d..c7a87870bc1a 100644 --- a/pom.xml +++ b/pom.xml @@ -303,7 +303,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.3.2 + 4.9.4.0 Max Low @@ -313,7 +313,7 @@ com.github.spotbugs spotbugs - 4.9.3 + 4.9.4 @@ -371,7 +371,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + 3.11.3 false From c8c6972fa0f154cbf2530cfd4baefdb780eb1b5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:43:38 +0000 Subject: [PATCH 271/701] Bump the hibernate group with 2 updates Bumps the hibernate group with 2 updates: [org.hibernate.validator:hibernate-validator](https://github.com/hibernate/hibernate-validator) and [org.hibernate.validator:hibernate-validator-cdi](https://github.com/hibernate/hibernate-validator). Updates `org.hibernate.validator:hibernate-validator` from 8.0.2.Final to 8.0.3.Final - [Changelog](https://github.com/hibernate/hibernate-validator/blob/8.0.3.Final/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-validator/compare/8.0.2.Final...8.0.3.Final) Updates `org.hibernate.validator:hibernate-validator-cdi` from 8.0.2.Final to 8.0.3.Final - [Changelog](https://github.com/hibernate/hibernate-validator/blob/8.0.3.Final/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-validator/compare/8.0.2.Final...8.0.3.Final) Updates `org.hibernate.validator:hibernate-validator-cdi` from 8.0.2.Final to 8.0.3.Final - [Changelog](https://github.com/hibernate/hibernate-validator/blob/8.0.3.Final/changelog.txt) - [Commits](https://github.com/hibernate/hibernate-validator/compare/8.0.2.Final...8.0.3.Final) --- updated-dependencies: - dependency-name: org.hibernate.validator:hibernate-validator dependency-version: 8.0.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: hibernate - dependency-name: org.hibernate.validator:hibernate-validator-cdi dependency-version: 8.0.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: hibernate - dependency-name: org.hibernate.validator:hibernate-validator-cdi dependency-version: 8.0.3.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: hibernate ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ab300551b21d..af9a4d7b21a3 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ 3.5.4 6.5.2 6.4.10.Final - 8.0.2.Final + 8.0.3.Final 42.7.7 10.22.0 8.11.4 From 103d7f754a8d0cf138478c87aaa7d4a726d4e734 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:44:10 +0000 Subject: [PATCH 272/701] Bump the test-tools group with 6 updates Bumps the test-tools group with 6 updates: | Package | From | To | | --- | --- | --- | | [io.netty:netty-buffer](https://github.com/netty/netty) | `4.2.3.Final` | `4.2.4.Final` | | [io.netty:netty-transport](https://github.com/netty/netty) | `4.2.3.Final` | `4.2.4.Final` | | [io.netty:netty-transport-native-unix-common](https://github.com/netty/netty) | `4.2.3.Final` | `4.2.4.Final` | | [io.netty:netty-common](https://github.com/netty/netty) | `4.2.3.Final` | `4.2.4.Final` | | [io.netty:netty-handler](https://github.com/netty/netty) | `4.2.3.Final` | `4.2.4.Final` | | [io.netty:netty-codec](https://github.com/netty/netty) | `4.2.3.Final` | `4.2.4.Final` | Updates `io.netty:netty-buffer` from 4.2.3.Final to 4.2.4.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.3.Final...netty-4.2.4.Final) Updates `io.netty:netty-transport` from 4.2.3.Final to 4.2.4.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.3.Final...netty-4.2.4.Final) Updates `io.netty:netty-transport-native-unix-common` from 4.2.3.Final to 4.2.4.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.3.Final...netty-4.2.4.Final) Updates `io.netty:netty-common` from 4.2.3.Final to 4.2.4.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.3.Final...netty-4.2.4.Final) Updates `io.netty:netty-handler` from 4.2.3.Final to 4.2.4.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.3.Final...netty-4.2.4.Final) Updates `io.netty:netty-codec` from 4.2.3.Final to 4.2.4.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.3.Final...netty-4.2.4.Final) --- updated-dependencies: - dependency-name: io.netty:netty-buffer dependency-version: 4.2.4.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport dependency-version: 4.2.4.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport-native-unix-common dependency-version: 4.2.4.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-common dependency-version: 4.2.4.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-handler dependency-version: 4.2.4.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-codec dependency-version: 4.2.4.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 3488fda26bf8..b8dbb4a6a7f1 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -870,32 +870,32 @@ io.netty netty-buffer - 4.2.3.Final + 4.2.4.Final io.netty netty-transport - 4.2.3.Final + 4.2.4.Final io.netty netty-transport-native-unix-common - 4.2.3.Final + 4.2.4.Final io.netty netty-common - 4.2.3.Final + 4.2.4.Final io.netty netty-handler - 4.2.3.Final + 4.2.4.Final io.netty netty-codec - 4.2.3.Final + 4.2.4.Final org.apache.velocity From 54752958a9f82ad5ec392fb366ff7710f022ac46 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:44:18 +0000 Subject: [PATCH 273/701] Bump commons-cli:commons-cli in the apache-commons group Bumps the apache-commons group with 1 update: [commons-cli:commons-cli](https://github.com/apache/commons-cli). Updates `commons-cli:commons-cli` from 1.9.0 to 1.10.0 - [Changelog](https://github.com/apache/commons-cli/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-cli/compare/rel/commons-cli-1.9.0...rel/commons-cli-1.10.0) --- updated-dependencies: - dependency-name: commons-cli:commons-cli dependency-version: 1.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ab300551b21d..a670498c5730 100644 --- a/pom.xml +++ b/pom.xml @@ -1471,7 +1471,7 @@ commons-cli commons-cli - 1.9.0 + 1.10.0 commons-codec From b78f570dbfa6a1303d644e39857037f8d99d4df2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:44:48 +0000 Subject: [PATCH 274/701] Bump the build-tools group with 3 updates Bumps the build-tools group with 3 updates: [com.github.spotbugs:spotbugs](https://github.com/spotbugs/spotbugs), [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) and [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin). Updates `com.github.spotbugs:spotbugs` from 4.9.3 to 4.9.4 - [Release notes](https://github.com/spotbugs/spotbugs/releases) - [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotbugs/spotbugs/compare/4.9.3...4.9.4) Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.9.3.2 to 4.9.4.0 - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.3.2...spotbugs-maven-plugin-4.9.4.0) Updates `org.apache.maven.plugins:maven-javadoc-plugin` from 3.11.2 to 3.11.3 - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.11.2...maven-javadoc-plugin-3.11.3) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs dependency-version: 4.9.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.4.0 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-version: 3.11.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 9a246b2b7acc..69daf0c75281 100644 --- a/pom.xml +++ b/pom.xml @@ -295,7 +295,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.3.2 + 4.9.4.0 Max Low @@ -305,7 +305,7 @@ com.github.spotbugs spotbugs - 4.9.3 + 4.9.4 @@ -363,7 +363,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + 3.11.3 false From 9bef7b58d694f6a83b094c846ad4d740bb4c99fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:45:53 +0000 Subject: [PATCH 275/701] Bump io.grpc:grpc-context from 1.74.0 to 1.75.0 Bumps [io.grpc:grpc-context](https://github.com/grpc/grpc-java) from 1.74.0 to 1.75.0. - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.74.0...v1.75.0) --- updated-dependencies: - dependency-name: io.grpc:grpc-context dependency-version: 1.75.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9a246b2b7acc..4325535d4120 100644 --- a/pom.xml +++ b/pom.xml @@ -1718,7 +1718,7 @@ io.grpc grpc-context - 1.74.0 + 1.75.0 com.google.http-client From c07bf84e059d5662f7f2b3d4053e7a4199f1f117 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:46:45 +0000 Subject: [PATCH 276/701] Bump the jakarta group with 2 updates Bumps the jakarta group with 2 updates: [jakarta.mail:jakarta.mail-api](https://github.com/jakartaee/mail-api) and org.eclipse.angus:jakarta.mail. Updates `jakarta.mail:jakarta.mail-api` from 2.1.3 to 2.1.4 - [Release notes](https://github.com/jakartaee/mail-api/releases) - [Commits](https://github.com/jakartaee/mail-api/compare/2.1.3...2.1.4) Updates `org.eclipse.angus:jakarta.mail` from 2.0.3 to 2.0.4 --- updated-dependencies: - dependency-name: jakarta.mail:jakarta.mail-api dependency-version: 2.1.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta - dependency-name: org.eclipse.angus:jakarta.mail dependency-version: 2.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ab300551b21d..0d34e2c1319a 100644 --- a/pom.xml +++ b/pom.xml @@ -1557,14 +1557,14 @@ jakarta.mail jakarta.mail-api - 2.1.3 + 2.1.4 provided org.eclipse.angus jakarta.mail - 2.0.3 + 2.0.4 jakarta.servlet From 5fc9d9d8e018f7a4658e203ae196d16419c6c924 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:48:16 +0000 Subject: [PATCH 277/701] Bump the spring group with 25 updates Bumps the spring group with 25 updates: | Package | From | To | | --- | --- | --- | | [org.springframework:spring-orm](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-core](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-beans](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-aop](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-context](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-context-support](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-tx](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-jdbc](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-web](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-expression](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework:spring-test](https://github.com/spring-projects/spring-framework) | `6.2.9` | `6.2.10` | | [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-tomcat](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-maven-plugin](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-cache](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-thymeleaf](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-web](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-data-rest](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-security](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-aop](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-actuator](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.boot:spring-boot-starter-log4j2](https://github.com/spring-projects/spring-boot) | `3.5.4` | `3.5.5` | | [org.springframework.security:spring-security-test](https://github.com/spring-projects/spring-security) | `6.5.2` | `6.5.3` | Updates `org.springframework:spring-orm` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-core` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-beans` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-aop` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-context` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-context-support` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-tx` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-jdbc` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-web` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-webmvc` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-expression` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-test` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-core` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-beans` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-aop` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-context` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-context-support` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-tx` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-jdbc` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-web` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-webmvc` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-expression` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework:spring-test` from 6.2.9 to 6.2.10 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.10) Updates `org.springframework.boot:spring-boot-starter-test` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.security:spring-security-test` from 6.5.2 to 6.5.3 - [Release notes](https://github.com/spring-projects/spring-security/releases) - [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc) - [Commits](https://github.com/spring-projects/spring-security/compare/6.5.2...6.5.3) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) --- updated-dependencies: - dependency-name: org.springframework:spring-orm dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.10 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.security:spring-security-test dependency-version: 6.5.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index ab300551b21d..2718aafa2e5c 100644 --- a/pom.xml +++ b/pom.xml @@ -19,9 +19,9 @@ 17 - 6.2.9 - 3.5.4 - 6.5.2 + 6.2.10 + 3.5.5 + 6.5.3 6.4.10.Final 8.0.2.Final 42.7.7 From 37ace6845ed0c6b111b21ef7c167167b8c8ef5fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:50:11 +0000 Subject: [PATCH 278/701] Bump jetty.version from 9.4.57.v20241219 to 9.4.58.v20250814 Bumps `jetty.version` from 9.4.57.v20241219 to 9.4.58.v20250814. Updates `org.eclipse.jetty:jetty-server` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-deploy` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-http` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-io` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-servlet` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-util` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-webapp` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty.http2:http2-common` from 9.4.57.v20241219 to 9.4.58.v20250814 --- updated-dependencies: - dependency-name: org.eclipse.jetty:jetty-server dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-deploy dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-http dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-io dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-servlet dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-util dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-webapp dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.http2:http2-common dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ab300551b21d..d145cbd13339 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 4.0.5 1.1.1 - 9.4.57.v20241219 + 9.4.58.v20250814 2.25.1 2.0.34 1.19.0 From cf3f8e9bb24d7498266219b44c68af3a1178eade Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:51:23 +0000 Subject: [PATCH 279/701] Bump commons-cli:commons-cli in the apache-commons group Bumps the apache-commons group with 1 update: [commons-cli:commons-cli](https://github.com/apache/commons-cli). Updates `commons-cli:commons-cli` from 1.9.0 to 1.10.0 - [Changelog](https://github.com/apache/commons-cli/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-cli/compare/rel/commons-cli-1.9.0...rel/commons-cli-1.10.0) --- updated-dependencies: - dependency-name: commons-cli:commons-cli dependency-version: 1.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9a246b2b7acc..bc588e2fb688 100644 --- a/pom.xml +++ b/pom.xml @@ -1473,7 +1473,7 @@ commons-cli commons-cli - 1.9.0 + 1.10.0 commons-codec From 13982618df3e9b7eaf88426bd3405bbb845e164d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:53:13 +0000 Subject: [PATCH 280/701] Bump jetty.version from 9.4.57.v20241219 to 9.4.58.v20250814 Bumps `jetty.version` from 9.4.57.v20241219 to 9.4.58.v20250814. Updates `org.eclipse.jetty:jetty-server` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-alpn-java-server` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-deploy` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-http` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-io` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-servlet` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-servlets` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-util` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-webapp` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty:jetty-xml` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty.http2:http2-common` from 9.4.57.v20241219 to 9.4.58.v20250814 Updates `org.eclipse.jetty.http2:http2-server` from 9.4.57.v20241219 to 9.4.58.v20250814 --- updated-dependencies: - dependency-name: org.eclipse.jetty:jetty-server dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-alpn-java-server dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-deploy dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-http dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-io dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-servlet dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-servlets dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-util dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-webapp dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-xml dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.http2:http2-common dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.http2:http2-server dependency-version: 9.4.58.v20250814 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9a246b2b7acc..50bf7ed1fbe3 100644 --- a/pom.xml +++ b/pom.xml @@ -37,7 +37,7 @@ 2.3.9 1.1.1 - 9.4.57.v20241219 + 9.4.58.v20250814 2.25.1 2.0.34 1.19.0 From 2c34dde3a84e260b4de61e65d68c01c04049bd97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 06:00:09 +0000 Subject: [PATCH 281/701] Bump org.ehcache:ehcache from 3.10.8 to 3.11.1 Bumps [org.ehcache:ehcache](https://github.com/ehcache/ehcache3) from 3.10.8 to 3.11.1. - [Release notes](https://github.com/ehcache/ehcache3/releases) - [Commits](https://github.com/ehcache/ehcache3/compare/v3.10.8...v3.11.1) --- updated-dependencies: - dependency-name: org.ehcache:ehcache dependency-version: 3.11.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9a246b2b7acc..7b10e4cef0eb 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ 42.7.7 8.11.4 - 3.10.8 + 3.11.1 2.31.0 2.19.2 From b099da697649386a258a40f37b06d66b9312e37c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 06:00:34 +0000 Subject: [PATCH 282/701] Bump jersey.version from 3.1.10 to 3.1.11 Bumps `jersey.version` from 3.1.10 to 3.1.11. Updates `org.glassfish.jersey.core:jersey-client` from 3.1.10 to 3.1.11 Updates `org.glassfish.jersey.inject:jersey-hk2` from 3.1.10 to 3.1.11 --- updated-dependencies: - dependency-name: org.glassfish.jersey.core:jersey-client dependency-version: 3.1.11 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.glassfish.jersey.inject:jersey-hk2 dependency-version: 3.1.11 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ab300551b21d..12edd902c0d2 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 1.81 8.0.1 - 3.1.10 + 3.1.11 2.9.0 From ccf253ddda16a26da13f0a957641dc971c9c4f06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 06:01:09 +0000 Subject: [PATCH 283/701] Bump net.minidev:json-smart from 2.5.2 to 2.6.0 Bumps [net.minidev:json-smart](https://github.com/netplex/json-smart-v2) from 2.5.2 to 2.6.0. - [Release notes](https://github.com/netplex/json-smart-v2/releases) - [Commits](https://github.com/netplex/json-smart-v2/compare/2.5.2...v2.6.0) --- updated-dependencies: - dependency-name: net.minidev:json-smart dependency-version: 2.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dspace-server-webapp/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 4cfe93f2ee45..84227b4a12fb 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -514,7 +514,7 @@ net.minidev json-smart - 2.5.2 + 2.6.0 From 58d26cf56b98068393aee8073ad8aa352450fc27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 06:02:42 +0000 Subject: [PATCH 284/701] Bump org.checkerframework:checker-qual from 3.49.5 to 3.50.0 Bumps [org.checkerframework:checker-qual](https://github.com/typetools/checker-framework) from 3.49.5 to 3.50.0. - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-3.49.5...checker-framework-3.50.0) --- updated-dependencies: - dependency-name: org.checkerframework:checker-qual dependency-version: 3.50.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ab300551b21d..ca07d250c2b2 100644 --- a/pom.xml +++ b/pom.xml @@ -1348,7 +1348,7 @@ org.checkerframework checker-qual - 3.49.5 + 3.50.0 2.19.2 From 8f4a3f963af8d9a12bca9f0980a5645053b07aec Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Mon, 25 Aug 2025 14:16:56 +0200 Subject: [PATCH 286/701] Implement logging for index out-of-range Add logging for out-of-range index when removing metadata values. (cherry picked from commit 7692b02b12ddcdc50ec22e3ccd060240734202ed) --- .../impl/MetadataValueRemovePatchOperation.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java index 1660a5455aea..18bc1df66c1c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java @@ -11,6 +11,8 @@ import java.util.Arrays; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -27,6 +29,8 @@ public abstract class MetadataValueRemovePatchOperation extends RemovePatchOperation { + private static final Logger log = LogManager.getLogger(); + @Override protected Class getArrayClassForEvaluation() { return MetadataValueRest[].class; @@ -42,7 +46,12 @@ protected void deleteValue(Context context, DSO source, String target, int index List mm = getDSpaceObjectService().getMetadata(source, metadata[0], metadata[1], metadata[2], Item.ANY); if (index != -1) { - getDSpaceObjectService().removeMetadataValues(context, source, Arrays.asList(mm.get(index))); + if (index < mm.size()) { + getDSpaceObjectService().removeMetadataValues(context, source, Arrays.asList(mm.get(index))); + } else { + log.warn("value of index ({}) is out of range of the metadata value list of size {} (target: {})", + index, mm.size(), target); + } } else { getDSpaceObjectService().clearMetadata(context, source, metadata[0], metadata[1], metadata[2], Item.ANY); } From 885c06b56f56dc305820432e3bfdca037a626104 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Mon, 25 Aug 2025 14:16:56 +0200 Subject: [PATCH 287/701] Implement logging for index out-of-range Add logging for out-of-range index when removing metadata values. (cherry picked from commit 7692b02b12ddcdc50ec22e3ccd060240734202ed) --- .../impl/MetadataValueRemovePatchOperation.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java index 1660a5455aea..18bc1df66c1c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/MetadataValueRemovePatchOperation.java @@ -11,6 +11,8 @@ import java.util.Arrays; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -27,6 +29,8 @@ public abstract class MetadataValueRemovePatchOperation extends RemovePatchOperation { + private static final Logger log = LogManager.getLogger(); + @Override protected Class getArrayClassForEvaluation() { return MetadataValueRest[].class; @@ -42,7 +46,12 @@ protected void deleteValue(Context context, DSO source, String target, int index List mm = getDSpaceObjectService().getMetadata(source, metadata[0], metadata[1], metadata[2], Item.ANY); if (index != -1) { - getDSpaceObjectService().removeMetadataValues(context, source, Arrays.asList(mm.get(index))); + if (index < mm.size()) { + getDSpaceObjectService().removeMetadataValues(context, source, Arrays.asList(mm.get(index))); + } else { + log.warn("value of index ({}) is out of range of the metadata value list of size {} (target: {})", + index, mm.size(), target); + } } else { getDSpaceObjectService().clearMetadata(context, source, metadata[0], metadata[1], metadata[2], Item.ANY); } From 376f49e99b54b89e03ceba7808d153db639f6e03 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Wed, 27 Aug 2025 15:01:35 +0200 Subject: [PATCH 288/701] 133421: Removed database connection leak on unsuccessful login (cherry picked from commit 0b0c9fc84c1cf3f51d008ace95ad0d94c26e9dbc) --- .../app/rest/security/OrcidLoginFilter.java | 1 + .../rest/security/StatelessLoginFilter.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java index 9fdef6b050f7..49dd7b87282f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java @@ -87,6 +87,7 @@ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServle String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url"); String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error"; response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection] + this.closeOpenContext(request); } else { super.unsuccessfulAuthentication(request, response, failed); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java index c95fce71c425..31f3d5d460ba 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java @@ -8,11 +8,14 @@ package org.dspace.app.rest.security; import java.io.IOException; +import java.sql.SQLException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.core.Context; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationManager; @@ -122,6 +125,27 @@ protected void unsuccessfulAuthentication(HttpServletRequest request, response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed!"); log.error("Authentication failed (status:{})", HttpServletResponse.SC_UNAUTHORIZED, failed); + this.closeOpenContext(request); + } + + /** + * Manually closes the open {@link Context} if one exists. We need to do this manually because + * {@link #continueChainBeforeSuccessfulAuthentication} is {@code false} by default, which prevents the + * {@link org.dspace.app.rest.filter.DSpaceRequestContextFilter} from being called. Without this call, the request + * would leave an open database connection. + * + * @param request The current request. + */ + protected void closeOpenContext(HttpServletRequest request) { + if (ContextUtil.isContextAvailable(request)) { + try (Context context = ContextUtil.obtainContext(request)) { + if (context != null && context.isValid()) { + context.complete(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } } From 286202e8df24b43667361f9837f1f72a97037eb7 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Wed, 27 Aug 2025 15:01:35 +0200 Subject: [PATCH 289/701] 133421: Removed database connection leak on unsuccessful login --- .../app/rest/security/OrcidLoginFilter.java | 1 + .../rest/security/StatelessLoginFilter.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java index 70496b9dba23..63ba9a2de0eb 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java @@ -89,6 +89,7 @@ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServle String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url"); String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error"; response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection] + this.closeOpenContext(request); } else { super.unsuccessfulAuthentication(request, response, failed); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java index cfae6bfcb42b..caead66df25f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.security; import java.io.IOException; +import java.sql.SQLException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -15,6 +16,8 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.core.Context; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -133,6 +136,27 @@ protected void unsuccessfulAuthentication(HttpServletRequest request, response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed!"); log.error("Authentication failed (status:{})", HttpServletResponse.SC_UNAUTHORIZED, failed); + this.closeOpenContext(request); + } + + /** + * Manually closes the open {@link Context} if one exists. We need to do this manually because + * {@link #continueChainBeforeSuccessfulAuthentication} is {@code false} by default, which prevents the + * {@link org.dspace.app.rest.filter.DSpaceRequestContextFilter} from being called. Without this call, the request + * would leave an open database connection. + * + * @param request The current request. + */ + protected void closeOpenContext(HttpServletRequest request) { + if (ContextUtil.isContextAvailable(request)) { + try (Context context = ContextUtil.obtainContext(request)) { + if (context != null && context.isValid()) { + context.complete(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } } From 66fc49800db5eaacdc6de9116c66701350bc7d37 Mon Sep 17 00:00:00 2001 From: Martin Walk Date: Wed, 20 Aug 2025 15:45:14 +0200 Subject: [PATCH 290/701] Fix #11074 export simple archive format with no collection (cherry picked from commit d282f92427f340a7f8ca68ca8316114ce0bdd524) --- .../app/itemexport/ItemExportServiceImpl.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java index 7c80e1ea7dc6..e5ca8fe688ce 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportServiceImpl.java @@ -352,7 +352,7 @@ protected void writeHandle(Context c, Item i, File destDir) /** * Create the 'collections' file. List handles of all Collections which - * contain this Item. The "owning" Collection is listed first. + * contain this Item. The "owning" Collection is listed first. * * @param item list collections holding this Item. * @param destDir write the file here. @@ -363,12 +363,14 @@ protected void writeCollections(Item item, File destDir) File outFile = new File(destDir, "collections"); if (outFile.createNewFile()) { try (PrintWriter out = new PrintWriter(new FileWriter(outFile))) { - String ownerHandle = item.getOwningCollection().getHandle(); - out.println(ownerHandle); + Collection owningCollection = item.getOwningCollection(); + // The owning collection is null for workspace and workflow items + if (owningCollection != null) { + out.println(owningCollection.getHandle()); + } for (Collection collection : item.getCollections()) { - String collectionHandle = collection.getHandle(); - if (!collectionHandle.equals(ownerHandle)) { - out.println(collectionHandle); + if (!collection.equals(owningCollection)) { + out.println(collection.getHandle()); } } } From ba5b147889937d2d357c7f533deaccf50ecd971a Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Fri, 9 May 2025 14:47:36 -0300 Subject: [PATCH 291/701] Update README.md (cherry picked from commit 7011556503f89fa2ea67370d15de4302e63ecd1f) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d93abe49948..c5e7e9277203 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# DSpace +# DSpace at LA Referencia [![Build Status](https://github.com/DSpace/DSpace/workflows/Build/badge.svg)](https://github.com/DSpace/DSpace/actions?query=workflow%3ABuild) From fc93298676b1d664bbdd27039b7ff39493225105 Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Mon, 14 Jul 2025 13:47:49 -0300 Subject: [PATCH 292/701] Update DSpace integration to use ROR API v2 (cherry picked from commit 53713629a617b8d963652b504f213c3a6239c9fb) --- ...itionalArrayElementAttributeProcessor.java | 132 ++++++++++++++++++ dspace/config/modules/external-providers.cfg | 2 +- dspace/config/spring/api/ror-integration.xml | 56 +++++++- 3 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java new file mode 100644 index 000000000000..af8af4d37ebf --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java @@ -0,0 +1,132 @@ +/** + * 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.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This Processor extracts values from a JSON array, but only when a condition + * on another attribute is met. For example, to extract all values of + * /names/value where /names/types contains "ror_display". + * + * Configurable via: + * pathToArray: e.g., /items/0/names + * elementAttribute: e.g., /value + * filterAttribute: e.g., /types + * requiredValueInFilter: e.g., ror_display + * + * Supports filtering when the filter attribute is either a JSON array or a single string. + * + * Example JSON: + * { + * "items": [{ + * "names": [ + * { "types": ["label", "ror_display"], "value": "Universidade Federal do Piauí" }, + * { "types": ["label"], "value": "UFPI" } + * ] + * }] + * } + * This processor can extract "Universidade Federal do Piauí" using proper configuration. + * + * Author: Jesiel (based on Mykhaylo Boychuk’s original processor) + */ +public class ConditionalArrayElementAttributeProcessor implements JsonPathMetadataProcessor { + + private static final Logger log = LogManager.getLogger(); + + private String pathToArray; + private String elementAttribute; + private String filterAttribute; + private String requiredValueInFilter; + + @Override + public Collection processMetadata(String json) { + System.out.println("pathToArray: " + pathToArray); + System.out.println("elementAttribute: " + elementAttribute); + System.out.println("filterAttribute: " + filterAttribute); + System.out.println("requiredValueInFilter: " + requiredValueInFilter); + JsonNode rootNode = convertStringJsonToJsonNode(json); + Collection results = new ArrayList<>(); + + if (rootNode == null) { + return results; + } + + Iterator array = rootNode.at(pathToArray).iterator(); + System.out.println("array: " + array.toString()); + while (array.hasNext()) { + JsonNode element = array.next(); + JsonNode filterNode = element.at(filterAttribute); + + boolean match = false; + + if (filterNode.isArray()) { + for (JsonNode filterValue : filterNode) { + if (requiredValueInFilter.equalsIgnoreCase(filterValue.textValue())) { + match = true; + break; + } + } + } else if (filterNode.isTextual()) { + if (requiredValueInFilter.equalsIgnoreCase(filterNode.textValue())) { + match = true; + } + } + + if (match) { + JsonNode valueNode = element.at(elementAttribute); + if (valueNode.isTextual()) { + results.add(valueNode.textValue()); + } else if (valueNode.isArray()) { + for (JsonNode item : valueNode) { + if (item.isTextual() && StringUtils.isNotBlank(item.textValue())) { + results.add(item.textValue()); + } + } + } + } + } + + return results; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process JSON response.", e); + return null; + } + } + + public void setPathToArray(String pathToArray) { + this.pathToArray = pathToArray; + } + + public void setElementAttribute(String elementAttribute) { + this.elementAttribute = elementAttribute; + } + + public void setFilterAttribute(String filterAttribute) { + this.filterAttribute = filterAttribute; + } + + public void setRequiredValueInFilter(String requiredValueInFilter) { + this.requiredValueInFilter = requiredValueInFilter; + } +} \ No newline at end of file diff --git a/dspace/config/modules/external-providers.cfg b/dspace/config/modules/external-providers.cfg index 04f5c54e9848..254207febdd6 100644 --- a/dspace/config/modules/external-providers.cfg +++ b/dspace/config/modules/external-providers.cfg @@ -98,5 +98,5 @@ datacite.timeout = 180000 #--------------------------- ROR -------------------------------# #---------------------------------------------------------------# -ror.orgunit-import.api-url = https://api.ror.org/organizations +ror.orgunit-import.api-url = https://api.ror.org/v2/organizations ################################################################# diff --git a/dspace/config/spring/api/ror-integration.xml b/dspace/config/spring/api/ror-integration.xml index ff554612052e..8d19d4d10b29 100644 --- a/dspace/config/spring/api/ror-integration.xml +++ b/dspace/config/spring/api/ror-integration.xml @@ -26,8 +26,17 @@ - + + + + + + + + + + @@ -42,15 +51,31 @@ - + + + + + + + + + + - + + + + + + + + @@ -66,7 +91,12 @@ - + + + + + + @@ -82,7 +112,14 @@ - + + + + + + + + @@ -90,7 +127,14 @@ - + + + + + + + + From abbfde862308e474d33b36fbfcc934fc988cbb1b Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Wed, 16 Jul 2025 09:28:58 -0300 Subject: [PATCH 293/701] refactoring and add addressLocality metadata (cherry picked from commit e89b00f8e64527f3b5faf2f54d699090657b1533) --- ...itionalArrayElementAttributeProcessor.java | 13 +++---- dspace/config/spring/api/ror-integration.xml | 35 +++++++++++++------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java index af8af4d37ebf..f813a34c89b5 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ConditionalArrayElementAttributeProcessor.java @@ -24,7 +24,7 @@ * /names/value where /names/types contains "ror_display". * * Configurable via: - * pathToArray: e.g., /items/0/names + * pathToArray: e.g., /names * elementAttribute: e.g., /value * filterAttribute: e.g., /types * requiredValueInFilter: e.g., ror_display @@ -35,12 +35,12 @@ * { * "items": [{ * "names": [ - * { "types": ["label", "ror_display"], "value": "Universidade Federal do Piauí" }, - * { "types": ["label"], "value": "UFPI" } + * { "types": ["label", "ror_display"], "value": "Instituto Federal do Piauí" }, + * { "types": ["acronym"], "value": "IFPI" } * ] * }] * } - * This processor can extract "Universidade Federal do Piauí" using proper configuration. + * This processor can extract "Instituto Federal do Piauí" using proper configuration. * * Author: Jesiel (based on Mykhaylo Boychuk’s original processor) */ @@ -55,10 +55,6 @@ public class ConditionalArrayElementAttributeProcessor implements JsonPathMetada @Override public Collection processMetadata(String json) { - System.out.println("pathToArray: " + pathToArray); - System.out.println("elementAttribute: " + elementAttribute); - System.out.println("filterAttribute: " + filterAttribute); - System.out.println("requiredValueInFilter: " + requiredValueInFilter); JsonNode rootNode = convertStringJsonToJsonNode(json); Collection results = new ArrayList<>(); @@ -67,7 +63,6 @@ public Collection processMetadata(String json) { } Iterator array = rootNode.at(pathToArray).iterator(); - System.out.println("array: " + array.toString()); while (array.hasNext()) { JsonNode element = array.next(); JsonNode filterNode = element.at(filterAttribute); diff --git a/dspace/config/spring/api/ror-integration.xml b/dspace/config/spring/api/ror-integration.xml index 8d19d4d10b29..f205817e8c7b 100644 --- a/dspace/config/spring/api/ror-integration.xml +++ b/dspace/config/spring/api/ror-integration.xml @@ -18,29 +18,29 @@ + + - - - + @@ -49,26 +49,24 @@ + - - - + - @@ -81,6 +79,7 @@ + @@ -89,9 +88,9 @@ + - @@ -102,6 +101,20 @@ + + + + + + + + + + + + + + @@ -110,9 +123,9 @@ + - @@ -125,9 +138,9 @@ + - @@ -140,6 +153,7 @@ + @@ -147,7 +161,6 @@ - From 3266f7538e089d954ce25cad7b002bb406c3b718 Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Wed, 16 Jul 2025 09:31:21 -0300 Subject: [PATCH 294/701] revert the README (cherry picked from commit d9e7b672e5b29ab54a8c2b35f0d4f966bab920b7) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c5e7e9277203..1d93abe49948 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# DSpace at LA Referencia +# DSpace [![Build Status](https://github.com/DSpace/DSpace/workflows/Build/badge.svg)](https://github.com/DSpace/DSpace/actions?query=workflow%3ABuild) From 2d8e209d7c5c53c4ec67b54d793404416f69a00d Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Wed, 16 Jul 2025 13:59:00 -0300 Subject: [PATCH 295/701] update integration tests to use ROR API v2 (cherry picked from commit afb4d2e54aaa9611f068b3db9d3a017cc3509bf6) --- .../RorImportMetadataSourceServiceIT.java | 61 +- .../org/dspace/app/rest/ror-record.json | 174 +- .../org/dspace/app/rest/ror-records.json | 3613 ++++++++--------- 3 files changed, 1718 insertions(+), 2130 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java index 9a8d14f3d658..84236fd58fe8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java @@ -45,6 +45,7 @@ public void tesGetRecords() throws Exception { CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + //ror-records.json is the result of a GET request to https://api.ror.org/v2/organizations at 16/07/2025. try (InputStream file = getClass().getResourceAsStream("ror-records.json")) { String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); @@ -59,21 +60,19 @@ public void tesGetRecords() throws Exception { ImportRecord record = recordsImported.iterator().next(); - assertThat(record.getValueList(), hasSize(11)); + assertThat(record.getValueList(), hasSize(9)); + + assertThat(record.getSingleValue("organization.legalName"), + is("University American College Skopje")); + assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/05hknds03")); + assertThat(record.getSingleValue("organization.alternateName"), is("UACS")); + assertThat(record.getSingleValue("organization.url"), is("https://uacs.edu.mk")); + assertThat(record.getSingleValue("dc.type"), is("education")); + assertThat(record.getSingleValue("organization.address.addressCountry"), is("MK")); + assertThat(record.getSingleValue("organization.address.addressLocality"), is("Skopje")); + assertThat(record.getSingleValue("organization.foundingDate"), is("2005")); + assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0004 0446 4427")); - assertThat( - record.getSingleValue("organization.legalName"), - is("The University of Texas") - ); - assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/02f6dcw23")); - assertThat(record.getSingleValue("organization.alternateName"), is("UTHSCSA")); - assertThat(record.getSingleValue("organization.url"), is("http://www.uthscsa.edu/")); - assertThat(record.getSingleValue("dc.type"), is("Education")); - assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); - assertThat(record.getSingleValue("organization.foundingDate"), is("1959")); - assertThat(record.getValue("organization", "identifier", "crossrefid"), hasSize(2)); - assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0001 0629 5880")); - assertThat(record.getSingleValue("organization.parentOrganization"), is("The University of Texas System")); } finally { liveImportClient.setHttpClient(originalHttpClient); @@ -96,7 +95,7 @@ public void tesCount() throws Exception { context.restoreAuthSystemState(); Integer count = rorServiceImpl.count("test"); - assertThat(count, equalTo(200)); + assertThat(count, equalTo(115409)); } finally { liveImportClient.setHttpClient(originalHttpClient); } @@ -110,6 +109,8 @@ public void tesGetRecord() throws Exception { try (InputStream file = getClass().getResourceAsStream("ror-record.json")) { + System.out.println("file = " + file.toString()); + String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); liveImportClient.setHttpClient(httpClient); @@ -117,22 +118,20 @@ public void tesGetRecord() throws Exception { when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); context.restoreAuthSystemState(); - ImportRecord record = rorServiceImpl.getRecord("https://ror.org/01sps7q28"); - assertThat(record.getValueList(), hasSize(9)); - assertThat( - record.getSingleValue("organization.legalName"), - is("The University of Texas Health Science Center at Tyler") - ); - assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/01sps7q28")); - assertThat(record.getSingleValue("organization.alternateName"), is("UTHSCT")); - assertThat(record.getSingleValue("organization.url"), - is("https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler")); - assertThat(record.getSingleValue("dc.type"), is("Healthcare")); + ImportRecord record = rorServiceImpl.getRecord("https://ror.org/02437s643"); + + assertThat(record.getValueList(), hasSize(10)); + assertThat(record.getSingleValue("organization.legalName"), + is("University of Illinois Chicago, Rockford campus")); + assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/02437s643")); + assertThat(record.getSingleValue("organization.alternateName"), is("UICOMR")); + assertThat(record.getSingleValue("organization.url"), is("https://www.uillinois.edu")); + assertThat(record.getSingleValue("dc.type"), is("education")); assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); - assertThat(record.getSingleValue("organization.foundingDate"), is("1947")); - assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0000 9704 5790")); - assertThat(record.getSingleValue("organization.parentOrganization"), is("The University of Texas System")); - + assertThat(record.getSingleValue("organization.address.addressLocality"), is("Rockford")); + assertThat(record.getSingleValue("organization.foundingDate"), is("1972")); + assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0000 9018 7542")); + assertThat(record.getSingleValue("organization.parentOrganization"), is("University of Illinois Chicago")); } finally { liveImportClient.setHttpClient(originalHttpClient); } @@ -152,7 +151,7 @@ public void tesGetRecordsCount() throws Exception { context.restoreAuthSystemState(); int tot = rorServiceImpl.getRecordsCount("test query"); - assertEquals(200, tot); + assertEquals(115409, tot); } finally { liveImportClient.setHttpClient(originalHttpClient); } diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json index 4d0cd97fd5b6..50df107f7d97 100644 --- a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json @@ -1,107 +1,93 @@ { - "id": "https://ror.org/01sps7q28", - "name": "The University of Texas Health Science Center at Tyler", - "email_address": null, - "ip_addresses": [ - - ], - "established": 1947, - "types": [ - "Healthcare" + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [], + "established": 1972, + "external_ids": [ + { + "all": [ + "grid.430864.d" + ], + "preferred": "grid.430864.d", + "type": "grid" + }, + { + "all": [ + "0000 0000 9018 7542" + ], + "preferred": null, + "type": "isni" + } ], - "relationships": [ + "id": "https://ror.org/02437s643", + "links": [ { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" + "type": "website", + "value": "https://www.uillinois.edu" } ], - "addresses": [ + "locations": [ { - "lat": 32.426014, - "lng": -95.212728, - "state": "Texas", - "state_code": "US-TX", - "city": "Tyler", - "geonames_city": { - "id": 4738214, - "city": "Tyler", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Smith County", - "id": 4729130, - "ascii_name": "Smith County", - "code": "US.TX.423" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "NA", + "continent_name": "North America", + "country_code": "US", + "country_name": "United States", + "country_subdivision_code": "IL", + "country_subdivision_name": "Illinois", + "lat": 42.27113, + "lng": -89.094, + "name": "Rockford" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 4907959 } ], - "links": [ - "https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler" - ], - "aliases": [ - "East Texas Tuberculosis Sanitarium", - "UT Health Northeast" - ], - "acronyms": [ - "UTHSCT" - ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_Tyler", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0000 9704 5790" - ] + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "UICOMR" }, - "OrgRef": { - "preferred": null, - "all": [ - "3446655" - ] + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "University of Illinois Chicago, Rockford campus" }, - "Wikidata": { - "preferred": null, - "all": [ - "Q7896437" - ] + { + "lang": "en", + "types": [ + "alias" + ], + "value": "University of Illinois at Rockford" + } + ], + "relationships": [ + { + "label": "University of Illinois Chicago", + "type": "parent", + "id": "https://ror.org/02mpq6x41" }, - "GRID": { - "preferred": "grid.267310.1", - "all": "grid.267310.1" + { + "label": "Swedish American Hospital", + "type": "related", + "id": "https://ror.org/05scd7d31" } - } -} + ], + "status": "active", + "types": [ + "education" + ] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json index 5f93bb7d07a0..46ffbbe9b844 100644 --- a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json @@ -1,2383 +1,1986 @@ { - "number_of_results": 200, - "time_taken": 12, + "number_of_results": 115409, + "time_taken": 63, "items": [ { - "id": "https://ror.org/02f6dcw23", - "name": "The University of Texas", - "email_address": null, - "ip_addresses": [ - - ], - "established": 1959, - "types": [ - "Education" - ], - "relationships": [ - { - "label": "Audie L. Murphy Memorial VA Hospital", - "type": "Related", - "id": "https://ror.org/035xhk118" - }, - { - "label": "San Antonio Military Medical Center", - "type": "Related", - "id": "https://ror.org/00m1mwc36" - }, - { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - } - ], - "addresses": [ - { - "lat": 29.508129, - "lng": -98.574025, - "state": "Texas", - "state_code": "US-TX", - "city": "San Antonio", - "geonames_city": { - "id": 4726206, - "city": "San Antonio", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Bexar County", - "id": 4674023, - "ascii_name": "Bexar County", - "code": "US.TX.029" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } - }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" } - ], - "links": [ - "http://www.uthscsa.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "UTHSCSA" - ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_San_Antonio", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "uacs.edu.mk" + ], + "established": 2005, + "external_ids": [ + { "all": [ - "0000 0001 0629 5880" - ] + "grid.445944.c" + ], + "preferred": "grid.445944.c", + "type": "grid" }, - "FundRef": { - "preferred": "100008635", + { "all": [ - "100008635", - "100008636" - ] + "0000 0004 0446 4427" + ], + "preferred": "0000 0004 0446 4427", + "type": "isni" }, - "OrgRef": { - "preferred": null, + { "all": [ - "1593427" - ] - }, - "Wikidata": { + "Q7894510" + ], "preferred": null, - "all": [ - "Q4005868" - ] - }, - "GRID": { - "preferred": "grid.267309.9", - "all": "grid.267309.9" + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/01sps7q28", - "name": "The University of Texas Health Science Center at Tyler", - "email_address": null, - "ip_addresses": [ - ], - "established": 1947, - "types": [ - "Healthcare" + "id": "https://ror.org/05hknds03", + "links": [ + { + "type": "website", + "value": "https://uacs.edu.mk" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/University_American_College_Skopje" + } ], - "relationships": [ + "locations": [ { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - } - ], - "addresses": [ - { - "lat": 32.426014, - "lng": -95.212728, - "state": "Texas", - "state_code": "US-TX", - "city": "Tyler", - "geonames_city": { - "id": 4738214, - "city": "Tyler", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Smith County", - "id": 4729130, - "ascii_name": "Smith County", - "code": "US.TX.423" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "MK", + "country_name": "North Macedonia", + "country_subdivision_code": null, + "country_subdivision_name": "Grad Skopje", + "lat": 41.99646, + "lng": 21.43141, + "name": "Skopje" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 785842 } ], - "links": [ - "https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler" - ], - "aliases": [ - "East Texas Tuberculosis Sanitarium", - "UT Health Northeast" - ], - "acronyms": [ - "UTHSCT" - ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_Tyler", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0000 9704 5790" - ] - }, - "OrgRef": { - "preferred": null, - "all": [ - "3446655" - ] - }, - "Wikidata": { - "preferred": null, - "all": [ - "Q7896437" - ] + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "UACS" }, - "GRID": { - "preferred": "grid.267310.1", - "all": "grid.267310.1" + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "University American College Skopje" } - } - }, - { - "id": "https://ror.org/05byvp690", - "name": "The University of Texas Southwestern Medical Center", - "email_address": null, - "ip_addresses": [ - ], - "established": 1943, + "relationships": [], + "status": "active", "types": [ - "Healthcare" + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [ + "3-5lab.fr" ], - "relationships": [ + "established": 2004, + "external_ids": [ { - "label": "Children's Medical Center", - "type": "Related", - "id": "https://ror.org/02ndk3y82" - }, - { - "label": "Parkland Memorial Hospital", - "type": "Related", - "id": "https://ror.org/0208r0146" - }, - { - "label": "VA North Texas Health Care System", - "type": "Related", - "id": "https://ror.org/01nzxq896" - }, - { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - }, - { - "label": "Institute for Exercise and Environmental Medicine", - "type": "Child", - "id": "https://ror.org/03gqc7y13" - }, - { - "label": "Texas Health Dallas", - "type": "Child", - "id": "https://ror.org/05k07p323" - } - ], - "addresses": [ - { - "lat": 32.812185, - "lng": -96.840174, - "state": "Texas", - "state_code": "US-TX", - "city": "Dallas", - "geonames_city": { - "id": 4684888, - "city": "Dallas", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Dallas County", - "id": 4684904, - "ascii_name": "Dallas County", - "code": "US.TX.113" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } - }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "all": [ + "grid.424877.a" + ], + "preferred": "grid.424877.a", + "type": "grid" } ], + "id": "https://ror.org/0509ggw88", "links": [ - "http://www.utsouthwestern.edu/" + { + "type": "website", + "value": "https://www.3-5lab.fr" + } ], - "aliases": [ - "UT Southwestern" + "locations": [ + { + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "FR", + "country_name": "France", + "country_subdivision_code": "IDF", + "country_subdivision_name": "Île-de-France", + "lat": 48.64026, + "lng": 2.23858, + "name": "Marcoussis" + }, + "geonames_id": 2995916 + } ], - "acronyms": [ - + "names": [ + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "III V Lab" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Southwestern_Medical_Center", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "facility" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "dnb.nl" + ], + "established": 1814, + "external_ids": [ + { "all": [ - "0000 0000 9482 7121" - ] + "501100014104" + ], + "preferred": "501100014104", + "type": "fundref" }, - "FundRef": { - "preferred": "100007914", + { "all": [ - "100007914", - "100010487", - "100008260" - ] + "grid.459463.9" + ], + "preferred": "grid.459463.9", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "617906" - ] - }, - "Wikidata": { + "0000 0004 0369 4300" + ], "preferred": null, - "all": [ - "Q2725999" - ] + "type": "isni" }, - "GRID": { - "preferred": "grid.267313.2", - "all": "grid.267313.2" + { + "all": [ + "Q1180205" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/019kgqr73", - "name": "The University of Texas at Arlington", - "email_address": "", - "ip_addresses": [ - ], - "established": 1895, - "types": [ - "Education" + "id": "https://ror.org/02fabx761", + "links": [ + { + "type": "website", + "value": "https://www.dnb.nl" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/De_Nederlandsche_Bank" + } ], - "relationships": [ + "locations": [ { - "label": "VA North Texas Health Care System", - "type": "Related", - "id": "https://ror.org/01nzxq896" - }, - { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - } - ], - "addresses": [ - { - "lat": 32.731, - "lng": -97.115, - "state": "Texas", - "state_code": "US-TX", - "city": "Arlington", - "geonames_city": { - "id": 4671240, - "city": "Arlington", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Tarrant County", - "id": 4735638, - "ascii_name": "Tarrant County", - "code": "US.TX.439" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "NL", + "country_name": "The Netherlands", + "country_subdivision_code": "NH", + "country_subdivision_name": "North Holland", + "lat": 52.37403, + "lng": 4.88969, + "name": "Amsterdam" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 2759794 } ], - "links": [ - "http://www.uta.edu/uta/" - ], - "aliases": [ - "UT Arlington" - ], - "acronyms": [ - "UTA" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Texas_at_Arlington", - "labels": [ + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "DNB" + }, + { + "lang": "nl", + "types": [ + "ror_display", + "label" + ], + "value": "De Nederlandsche Bank" + }, { - "label": "Université du Texas à Arlington", - "iso639": "fr" + "lang": "en", + "types": [ + "alias" + ], + "value": "Dutch Bank" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "relationships": [], + "status": "active", + "types": [ + "funder", + "other" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "kfupm.edu.sa" + ], + "established": 1963, + "external_ids": [ + { "all": [ - "0000 0001 2181 9515" - ] - }, - "FundRef": { + "501100004055" + ], "preferred": null, - "all": [ - "100009497" - ] + "type": "fundref" }, - "OrgRef": { - "preferred": null, + { "all": [ - "906409" - ] + "grid.412135.0" + ], + "preferred": "grid.412135.0", + "type": "grid" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q1230739" - ] + "0000 0001 1091 0356" + ], + "preferred": null, + "type": "isni" }, - "GRID": { - "preferred": "grid.267315.4", - "all": "grid.267315.4" + { + "all": [ + "Q4116241" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/051smbs96", - "name": "The University of Texas of the Permian Basin", - "email_address": null, - "ip_addresses": [ - ], - "established": 1973, - "types": [ - "Education" + "id": "https://ror.org/03yez3163", + "links": [ + { + "type": "website", + "value": "https://www.kfupm.edu.sa" + }, + { + "type": "wikipedia", + "value": "http://en.wikipedia.org/wiki/King_Fahd_University_of_Petroleum_and_Minerals" + } ], - "relationships": [ + "locations": [ { - "label": "The University of Texas System", - "type": "Parent", - "id": "https://ror.org/01gek1696" - } - ], - "addresses": [ - { - "lat": 31.889444, - "lng": -102.329531, - "state": "Texas", - "state_code": "US-TX", - "city": "Odessa", - "geonames_city": { - "id": 5527554, - "city": "Odessa", - "geonames_admin1": { - "name": "Texas", - "id": 4736286, - "ascii_name": "Texas", - "code": "US.TX" - }, - "geonames_admin2": { - "name": "Ector County", - "id": 5520910, - "ascii_name": "Ector County", - "code": "US.TX.135" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "SA", + "country_name": "Saudi Arabia", + "country_subdivision_code": "04", + "country_subdivision_name": "Eastern Province", + "lat": 26.28864, + "lng": 50.11396, + "name": "Dhahran" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 107797 } ], - "links": [ - "http://www.utpb.edu/" - ], - "aliases": [ - "UT Permian Basin" - ], - "acronyms": [ - "UTPB" + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "KFUPM" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "King Fahd University of Petroleum and Minerals" + }, + { + "lang": "ar", + "types": [ + "label" + ], + "value": "جامعة الملك فهد للبترول والمعادن" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Texas_of_the_Permian_Basin", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "education", + "funder" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [], + "established": null, + "external_ids": [ + { "all": [ - "0000 0000 9140 1491" - ] - }, - "OrgRef": { + "100006445" + ], "preferred": null, - "all": [ - "1419441" - ] + "type": "fundref" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q2495935" - ] - }, - "GRID": { - "preferred": "grid.267328.a", - "all": "grid.267328.a" + "grid.457570.4" + ], + "preferred": "grid.457570.4", + "type": "grid" } - } - }, - { - "id": "https://ror.org/044vy1d05", - "name": "Tokushima University", - "email_address": "", - "ip_addresses": [ - ], - "established": 1949, - "types": [ - "Education" + "id": "https://ror.org/043trmd87", + "links": [ + { + "type": "website", + "value": "http://chm.pse.umass.edu/" + } ], - "relationships": [ + "locations": [ { - "label": "Tokushima University Hospital", - "type": "Related", - "id": "https://ror.org/021ph5e41" - } - ], - "addresses": [ - { - "lat": 34.07, - "lng": 134.56, - "state": null, - "state_code": null, - "city": "Tokushima", - "geonames_city": { - "id": 1850158, - "city": "Tokushima", - "geonames_admin1": { - "name": "Tokushima", - "id": 1850157, - "ascii_name": "Tokushima", - "code": "JP.39" - }, - "geonames_admin2": { - "name": "Tokushima Shi", - "id": 1850156, - "ascii_name": "Tokushima Shi", - "code": "JP.39.1850156" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "NA", + "continent_name": "North America", + "country_code": "US", + "country_name": "United States", + "country_subdivision_code": "MA", + "country_subdivision_name": "Massachusetts", + "lat": 42.37537, + "lng": -72.51925, + "name": "Amherst Center" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 1861060 + "geonames_id": 4929023 } ], - "links": [ - "https://www.tokushima-u.ac.jp/" - ], - "aliases": [ - "Tokushima Daigaku", - "University of Tokushima" - ], - "acronyms": [ - + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "CHM" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Center for Hierarchical Manufacturing" + } ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Tokushima", - "labels": [ + "relationships": [ + { + "label": "U.S. National Science Foundation", + "type": "related", + "id": "https://ror.org/021nxhr62" + }, { - "label": "徳島大学", - "iso639": "ja" + "label": "University of Massachusetts Amherst", + "type": "related", + "id": "https://ror.org/0072zz521" } ], - "country": { - "country_name": "Japan", - "country_code": "JP" + "status": "active", + "types": [ + "funder", + "nonprofit" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "musashino-u.ac.jp" + ], + "established": 1924, + "external_ids": [ + { "all": [ - "0000 0001 1092 3579" - ] + "100019640" + ], + "preferred": "100019640", + "type": "fundref" }, - "FundRef": { - "preferred": null, + { "all": [ - "501100005623" - ] + "grid.411867.d" + ], + "preferred": "grid.411867.d", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "15696836" - ] - }, - "Wikidata": { + "0000 0001 0356 8417" + ], "preferred": null, - "all": [ - "Q1150231" - ] + "type": "isni" }, - "GRID": { - "preferred": "grid.267335.6", - "all": "grid.267335.6" + { + "all": [ + "Q6940182" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/03np13864", - "name": "University of Trinidad and Tobago", - "email_address": null, - "ip_addresses": [ - ], - "established": 2004, - "types": [ - "Education" + "id": "https://ror.org/04bcbax71", + "links": [ + { + "type": "website", + "value": "https://www.musashino-u.ac.jp" + }, + { + "type": "wikipedia", + "value": "http://en.wikipedia.org/wiki/Musashino_University" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 10.616667, - "lng": -61.216667, - "state": null, - "state_code": null, - "city": "Arima", - "geonames_city": { - "id": 3575051, - "city": "Arima", - "geonames_admin1": { - "name": "Borough of Arima", - "id": 3575052, - "ascii_name": "Borough of Arima", - "code": "TT.01" - }, - "geonames_admin2": { - "name": null, - "id": null, - "ascii_name": null, - "code": null - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "JP", + "country_name": "Japan", + "country_subdivision_code": "13", + "country_subdivision_name": "Tokyo", + "lat": 35.6895, + "lng": 139.69171, + "name": "Tokyo" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 3573591 + "geonames_id": 1850147 } ], - "links": [ - "https://utt.edu.tt/" - ], - "aliases": [ - - ], - "acronyms": [ - "UTT" - ], - "status": "active", - "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Trinidad_and_Tobago", - "labels": [ + "names": [ + { + "lang": null, + "types": [ + "alias" + ], + "value": "Musashino Daigaku" + }, + { + "lang": null, + "types": [ + "ror_display", + "label" + ], + "value": "Musashino University" + }, { - "label": "Universidad de Trinidad y Tobago", - "iso639": "es" + "lang": "ja", + "types": [ + "label" + ], + "value": "武蔵野大学" } ], - "country": { - "country_name": "Trinidad and Tobago", - "country_code": "TT" + "relationships": [], + "status": "active", + "types": [ + "education", + "funder" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "hansung.ac.kr" + ], + "established": 1972, + "external_ids": [ + { "all": [ - "0000 0000 9490 0886" - ] - }, - "OrgRef": { + "501100002491" + ], "preferred": null, + "type": "fundref" + }, + { "all": [ - "8706288" - ] + "grid.444079.a" + ], + "preferred": "grid.444079.a", + "type": "grid" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q648244" - ] + "0000 0004 0532 678X" + ], + "preferred": null, + "type": "isni" }, - "GRID": { - "preferred": "grid.267355.0", - "all": "grid.267355.0" + { + "all": [ + "Q482765" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/04wn28048", - "name": "University of Tulsa", - "email_address": "", - "ip_addresses": [ - ], - "established": 1894, - "types": [ - "Education" + "id": "https://ror.org/048m9x696", + "links": [ + { + "type": "website", + "value": "https://www.hansung.ac.kr" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Hansung_University" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 36.152222, - "lng": -95.946389, - "state": "Oklahoma", - "state_code": "US-OK", - "city": "Tulsa", - "geonames_city": { - "id": 4553433, - "city": "Tulsa", - "geonames_admin1": { - "name": "Oklahoma", - "id": 4544379, - "ascii_name": "Oklahoma", - "code": "US.OK" - }, - "geonames_admin2": { - "name": "Tulsa County", - "id": 4553440, - "ascii_name": "Tulsa County", - "code": "US.OK.143" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "KR", + "country_name": "South Korea", + "country_subdivision_code": "11", + "country_subdivision_name": "Seoul", + "lat": 37.566, + "lng": 126.9784, + "name": "Seoul" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 1835848 } ], - "links": [ - "http://utulsa.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "TU" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Tulsa", - "labels": [ + "names": [ + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Hansung University" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Hansung Woman's University" + }, { - "label": "Université de tulsa", - "iso639": "fr" + "lang": "ko", + "types": [ + "label" + ], + "value": "한성대학교" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "relationships": [], + "status": "active", + "types": [ + "education", + "funder" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "tuh.ie" + ], + "established": 1996, + "external_ids": [ + { "all": [ - "0000 0001 2160 264X" - ] + "grid.413305.0" + ], + "preferred": "grid.413305.0", + "type": "grid" }, - "FundRef": { - "preferred": "100007147", + { "all": [ - "100007147", - "100006455" - ] - }, - "OrgRef": { + "0000 0004 0617 5936" + ], "preferred": null, - "all": [ - "32043" - ] + "type": "isni" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q1848657" - ] - }, - "GRID": { - "preferred": "grid.267360.6", - "all": "grid.267360.6" + "Q7680014" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/04scfb908", - "name": "Alfred Health", - "email_address": null, - "ip_addresses": [ - ], - "established": 1871, - "types": [ - "Healthcare" + "id": "https://ror.org/01fvmtt37", + "links": [ + { + "type": "website", + "value": "https://www.tuh.ie" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Tallaght_Hospital" + } ], - "relationships": [ + "locations": [ { - "label": "Caulfield Hospital", - "type": "Child", - "id": "https://ror.org/01fcxf261" - }, - { - "label": "Melbourne Sexual Health Centre", - "type": "Child", - "id": "https://ror.org/013fdz725" - }, - { - "label": "National Trauma Research Institute", - "type": "Child", - "id": "https://ror.org/048t93218" - }, - { - "label": "The Alfred Hospital", - "type": "Child", - "id": "https://ror.org/01wddqe20" - } - ], - "addresses": [ - { - "lat": -37.845542, - "lng": 144.981632, - "state": "Victoria", - "state_code": "AU-VIC", - "city": "Melbourne", - "geonames_city": { - "id": 2158177, - "city": "Melbourne", - "geonames_admin1": { - "name": "Victoria", - "id": 2145234, - "ascii_name": "Victoria", - "code": "AU.07" - }, - "geonames_admin2": { - "name": "Melbourne", - "id": 7839805, - "ascii_name": "Melbourne", - "code": "AU.07.24600" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "IE", + "country_name": "Ireland", + "country_subdivision_code": "L", + "country_subdivision_name": "Leinster", + "lat": 53.33306, + "lng": -6.24889, + "name": "Dublin" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 2077456 + "geonames_id": 2964574 } ], - "links": [ - "http://www.alfred.org.au/" - ], - "aliases": [ - - ], - "acronyms": [ - - ], - "status": "active", - "wikipedia_url": "", - "labels": [ - - ], - "country": { - "country_name": "Australia", - "country_code": "AU" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0004 0432 5259" - ] + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "AMNCH" }, - "FundRef": { - "preferred": null, - "all": [ - "501100002716" - ] + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Adelaide and Meath Hospital, Dublin, incorporating the National Children's Hospital" + }, + { + "lang": "ga", + "types": [ + "alias" + ], + "value": "Ospidéal Adelaide agus na Mí, Baile Átha Cliath, ina gcorpraítear Ospidéal Náisiúnta na Leanaí" }, - "GRID": { - "preferred": "grid.267362.4", - "all": "grid.267362.4" + { + "lang": "ga", + "types": [ + "label" + ], + "value": "Ospidéal Ollscoile Thamhlachta" + }, + { + "lang": "ga", + "types": [ + "alias" + ], + "value": "Ospidéal Thamhlachta" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Tallaght Hospital" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Tallaght University Hospital" } - } - }, - { - "id": "https://ror.org/02c2f8975", - "name": "University of Ulsan", - "email_address": null, - "ip_addresses": [ - - ], - "established": 1970, - "types": [ - "Education" ], "relationships": [ { - "label": "Ulsan University Hospital", - "type": "Related", - "id": "https://ror.org/03sab2a45" - } - ], - "addresses": [ - { - "lat": 35.542772, - "lng": 129.256725, - "state": null, - "state_code": null, - "city": "Ulsan", - "geonames_city": { - "id": 1833747, - "city": "Ulsan", - "geonames_admin1": { - "name": "Ulsan", - "id": 1833742, - "ascii_name": "Ulsan", - "code": "KR.21" - }, - "geonames_admin2": { - "name": null, - "id": null, - "ascii_name": null, - "code": null - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } - }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 1835841 + "label": "Trinity College Dublin", + "type": "related", + "id": "https://ror.org/02tyrky19" } ], - "links": [ - "http://en.ulsan.ac.kr/contents/main/" - ], - "aliases": [ - - ], - "acronyms": [ - "UOU" - ], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Ulsan", - "labels": [ - { - "label": "울산대학교", - "iso639": "ko" + "types": [ + "healthcare" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" } - ], - "country": { - "country_name": "South Korea", - "country_code": "KR" }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "mseuf.edu.ph" + ], + "established": 1947, + "external_ids": [ + { "all": [ - "0000 0004 0533 4667" - ] + "grid.448687.1" + ], + "preferred": "grid.448687.1", + "type": "grid" }, - "FundRef": { - "preferred": null, + { "all": [ - "501100002568" - ] + "0000 0004 0639 6528" + ], + "preferred": null, + "type": "isni" }, - "OrgRef": { - "preferred": "10458246", + { "all": [ - "10458246", - "15162872" - ] - }, - "Wikidata": { + "Q3578221" + ], "preferred": null, - "all": [ - "Q491717" - ] - }, - "GRID": { - "preferred": "grid.267370.7", - "all": "grid.267370.7" + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/010acrp16", - "name": "University of West Alabama", - "email_address": null, - "ip_addresses": [ - ], - "established": 1835, - "types": [ - "Education" + "id": "https://ror.org/02fhfq388", + "links": [ + { + "type": "website", + "value": "https://mseuf.edu.ph" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Enverga_University" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 32.59, - "lng": -88.186, - "state": "Alabama", - "state_code": "US-AL", - "city": "Livingston", - "geonames_city": { - "id": 4073383, - "city": "Livingston", - "geonames_admin1": { - "name": "Alabama", - "id": 4829764, - "ascii_name": "Alabama", - "code": "US.AL" - }, - "geonames_admin2": { - "name": "Sumter County", - "id": 4092386, - "ascii_name": "Sumter County", - "code": "US.AL.119" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "PH", + "country_name": "Philippines", + "country_subdivision_code": "40", + "country_subdivision_name": "Calabarzon", + "lat": 13.93139, + "lng": 121.61722, + "name": "Lucena City" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 1705357 } ], - "links": [ - "http://www.uwa.edu/" - ], - "aliases": [ - "Livingston Female Academy" - ], - "acronyms": [ - "UWA" + "names": [ + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Enverga University" + }, + { + "lang": null, + "types": [ + "acronym" + ], + "value": "MSEUF" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Manuel S. Enverga University Foundation" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Alabama", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "plus.ac.at" + ], + "established": 1622, + "external_ids": [ + { "all": [ - "0000 0000 9963 9197" - ] - }, - "OrgRef": { + "501100005644" + ], "preferred": null, + "type": "fundref" + }, + { "all": [ - "2425212" - ] + "grid.7039.d" + ], + "preferred": "grid.7039.d", + "type": "grid" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q637346" - ] + "0000 0001 1015 6330" + ], + "preferred": null, + "type": "isni" }, - "GRID": { - "preferred": "grid.267434.0", - "all": "grid.267434.0" + { + "all": [ + "Q27265" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/002w4zy91", - "name": "University of West Florida", - "email_address": null, - "ip_addresses": [ - ], - "established": 1963, - "types": [ - "Education" + "id": "https://ror.org/05gs8cd61", + "links": [ + { + "type": "website", + "value": "https://www.plus.ac.at/" + }, + { + "type": "wikipedia", + "value": "http://en.wikipedia.org/wiki/University_of_Salzburg" + } ], - "relationships": [ + "locations": [ { - "label": "State University System of Florida", - "type": "Parent", - "id": "https://ror.org/05sqd3t97" - } - ], - "addresses": [ - { - "lat": 30.549493, - "lng": -87.21812, - "state": "Florida", - "state_code": "US-FL", - "city": "Pensacola", - "geonames_city": { - "id": 4168228, - "city": "Pensacola", - "geonames_admin1": { - "name": "Florida", - "id": 4155751, - "ascii_name": "Florida", - "code": "US.FL" - }, - "geonames_admin2": { - "name": "Escambia County", - "id": 4154550, - "ascii_name": "Escambia County", - "code": "US.FL.033" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "AT", + "country_name": "Austria", + "country_subdivision_code": "5", + "country_subdivision_name": "Salzburg", + "lat": 47.79941, + "lng": 13.04399, + "name": "Salzburg" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 2766824 } ], - "links": [ - "http://uwf.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "UWF" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Florida", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0001 2112 2427" - ] - }, - "FundRef": { - "preferred": null, - "all": [ - "100009842" - ] + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "PLUS" }, - "OrgRef": { - "preferred": null, - "all": [ - "750756" - ] + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Paris Lodron University of Salzburg" }, - "Wikidata": { - "preferred": null, - "all": [ - "Q659255" - ] + { + "lang": "de", + "types": [ + "label" + ], + "value": "Paris-Lodron-Universität Salzburg" }, - "GRID": { - "preferred": "grid.267436.2", - "all": "grid.267436.2" + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "University of Salzburg" } - } + ], + "relationships": [], + "status": "active", + "types": [ + "education", + "funder" + ] }, { - "id": "https://ror.org/01cqxk816", - "name": "University of West Georgia", - "email_address": null, - "ip_addresses": [ - + "admin": { + "created": { + "date": "2024-11-18", + "schema_version": "2.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [ + "hch.tums.ac.ir" ], - "established": 1906, - "types": [ - "Education" + "established": null, + "external_ids": [], + "id": "https://ror.org/04hgqjy83", + "links": [ + { + "type": "website", + "value": "https://hch.tums.ac.ir" + } ], - "relationships": [ + "locations": [ { - "label": "University System of Georgia", - "type": "Parent", - "id": "https://ror.org/017wcm924" - } - ], - "addresses": [ - { - "lat": 33.573357, - "lng": -85.099593, - "state": "Georgia", - "state_code": "US-GA", - "city": "Carrollton", - "geonames_city": { - "id": 4186416, - "city": "Carrollton", - "geonames_admin1": { - "name": "Georgia", - "id": 4197000, - "ascii_name": "Georgia", - "code": "US.GA" - }, - "geonames_admin2": { - "name": "Carroll County", - "id": 4186396, - "ascii_name": "Carroll County", - "code": "US.GA.045" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "IR", + "country_name": "Iran", + "country_subdivision_code": "23", + "country_subdivision_name": "Tehran", + "lat": 35.69439, + "lng": 51.42151, + "name": "Tehran" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 112931 } ], - "links": [ - "http://www.westga.edu/" - ], - "aliases": [ - + "names": [ + { + "lang": "en", + "types": [ + "label", + "ror_display" + ], + "value": "Hakim Children Hospital" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "Hakim Children's Hospital" + }, + { + "lang": "fa", + "types": [ + "label" + ], + "value": "بیمارستان کودکان حکیم" + }, + { + "lang": "fa", + "types": [ + "alias" + ], + "value": "بیمارستان کودکان حکیم دانشگاه علوم پزشکی تهران" + } ], - "acronyms": [ - "UWG" + "relationships": [ + { + "label": "Tehran University of Medical Sciences", + "type": "related", + "id": "https://ror.org/01c4pz451" + } ], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Georgia", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "healthcare" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "pstu.ac.bd" + ], + "established": 2000, + "external_ids": [ + { "all": [ - "0000 0001 2223 6696" - ] + "501100014587" + ], + "preferred": "501100014587", + "type": "fundref" }, - "FundRef": { - "preferred": null, + { "all": [ - "100007922" - ] + "grid.443081.a" + ], + "preferred": "grid.443081.a", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "595315" - ] - }, - "Wikidata": { + "0000 0004 0489 3643" + ], "preferred": null, - "all": [ - "Q2495945" - ] + "type": "isni" }, - "GRID": { - "preferred": "grid.267437.3", - "all": "grid.267437.3" + { + "all": [ + "Q7148748" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/03c8vvr84", - "name": "University of Western States", - "email_address": null, - "ip_addresses": [ - ], - "established": 1904, - "types": [ - "Education" + "id": "https://ror.org/03m50n726", + "links": [ + { + "type": "website", + "value": "https://www.pstu.ac.bd" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Patuakhali_Science_and_Technology_University" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 45.543351, - "lng": -122.523973, - "state": "Oregon", - "state_code": "US-OR", - "city": "Portland", - "geonames_city": { - "id": 5746545, - "city": "Portland", - "geonames_admin1": { - "name": "Oregon", - "id": 5744337, - "ascii_name": "Oregon", - "code": "US.OR" - }, - "geonames_admin2": { - "name": "Multnomah County", - "id": 5742126, - "ascii_name": "Multnomah County", - "code": "US.OR.051" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "BD", + "country_name": "Bangladesh", + "country_subdivision_code": "A", + "country_subdivision_name": "Barisal Division", + "lat": 22.33333, + "lng": 90.33333, + "name": "Patuakhali" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 1337216 } ], - "links": [ - "http://www.uws.edu/" - ], - "aliases": [ - "Western States Chiropractic College" - ], - "acronyms": [ - "UWS" + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "PSTU" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Patuakhali Science and Technology University" + }, + { + "lang": "bn", + "types": [ + "label" + ], + "value": "পটুয়াখালী বিজ্ঞান ও প্রযুক্তি বিশ্ববিদ্যালয়" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Western_States", - "labels": [ - - ], - "country": { - "country_name": "United States", - "country_code": "US" + "types": [ + "education", + "funder" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "aih-net.com" + ], + "established": 1918, + "external_ids": [ + { "all": [ - "0000 0004 0455 9493" - ] + "grid.413984.3" + ], + "preferred": "grid.413984.3", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "1655050" - ] - }, - "Wikidata": { + "Q11666229" + ], "preferred": null, - "all": [ - "Q7896612" - ] - }, - "GRID": { - "preferred": "grid.267451.3", - "all": "grid.267451.3" + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/03fmjzx88", - "name": "University of Winchester", - "email_address": null, - "ip_addresses": [ - ], - "established": 1840, - "types": [ - "Education" + "id": "https://ror.org/04tg98e93", + "links": [ + { + "type": "website", + "value": "https://aih-net.com" + } ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 51.060338, - "lng": -1.325418, - "state": null, - "state_code": null, - "city": "Winchester", - "geonames_city": { - "id": 2633858, - "city": "Winchester", - "geonames_admin1": { - "name": "England", - "id": 6269131, - "ascii_name": "England", - "code": "GB.ENG" - }, - "geonames_admin2": { - "name": "Hampshire", - "id": 2647554, - "ascii_name": "Hampshire", - "code": "GB.ENG.F2" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": "SOUTH EAST (ENGLAND)", - "code": "UKJ" - }, - "nuts_level2": { - "name": "Hampshire and Isle of Wight", - "code": "UKJ3" - }, - "nuts_level3": { - "name": "Central Hampshire", - "code": "UKJ36" - } + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "JP", + "country_name": "Japan", + "country_subdivision_code": "40", + "country_subdivision_name": "Fukuoka", + "lat": 33.63654, + "lng": 130.68678, + "name": "Iizuka" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 2635167 + "geonames_id": 1861835 } ], - "links": [ - "http://www.winchester.ac.uk/pages/home.aspx" - ], - "aliases": [ - - ], - "acronyms": [ - + "names": [ + { + "lang": null, + "types": [ + "ror_display", + "label" + ], + "value": "Aso Iizuka Hospital" + }, + { + "lang": "ja", + "types": [ + "label" + ], + "value": "飯塚病院" + } ], + "relationships": [], "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Winchester", - "labels": [ - - ], - "country": { - "country_name": "United Kingdom", - "country_code": "GB" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0000 9422 2878" - ] + "types": [ + "healthcare" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" }, - "FundRef": { - "preferred": null, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [ + "imsciences.edu.pk" + ], + "established": 1995, + "external_ids": [ + { "all": [ - "100010057" - ] + "grid.444989.c" + ], + "preferred": "grid.444989.c", + "type": "grid" }, - "HESA": { - "preferred": null, + { "all": [ - "0021" - ] - }, - "UCAS": { + "0000 0004 0609 2495" + ], "preferred": null, - "all": [ - "W76" - ] + "type": "isni" }, - "UKPRN": { - "preferred": null, + { "all": [ - "10003614" - ] - }, - "OrgRef": { + "Q15983147" + ], "preferred": null, - "all": [ - "3140939" - ] + "type": "wikidata" + } + ], + "id": "https://ror.org/02m8e1r74", + "links": [ + { + "type": "website", + "value": "https://imsciences.edu.pk" }, - "Wikidata": { - "preferred": null, - "all": [ - "Q3551690" - ] + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Institute_of_Management_Sciences_(Peshawar)" + } + ], + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "PK", + "country_name": "Pakistan", + "country_subdivision_code": "KP", + "country_subdivision_name": "Khyber Pakhtunkhwa", + "lat": 34.008, + "lng": 71.57849, + "name": "Peshawar" + }, + "geonames_id": 1168197 + } + ], + "names": [ + { + "lang": null, + "types": [ + "alias" + ], + "value": "IMSciences" }, - "GRID": { - "preferred": "grid.267454.6", - "all": "grid.267454.6" + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Institute of Management Sciences Peshawar" } - } - }, - { - "id": "https://ror.org/01gw3d370", - "name": "University of Windsor", - "email_address": "", - "ip_addresses": [ - ], - "established": 1857, + "relationships": [], + "status": "active", "types": [ - "Education" + "education" + ] + }, + { + "admin": { + "created": { + "date": "2022-08-31", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [ + "galgotiacollege.edu" ], - "relationships": [ - - ], - "addresses": [ - { - "lat": 42.305196, - "lng": -83.067483, - "state": "Ontario", - "state_code": "CA-ON", - "city": "Windsor", - "geonames_city": { - "id": 6182962, - "city": "Windsor", - "geonames_admin1": { - "name": "Ontario", - "id": 6093943, - "ascii_name": "Ontario", - "code": "CA.08" - }, - "geonames_admin2": { - "name": null, - "id": null, - "ascii_name": null, - "code": null - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } - }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6251999 + "established": 1999, + "external_ids": [ + { + "all": [ + "0000 0004 1774 2078" + ], + "preferred": "0000 0004 1774 2078", + "type": "isni" } ], + "id": "https://ror.org/04a85ht85", "links": [ - "http://www.uwindsor.ca/" - ], - "aliases": [ - "UWindsor", - "Assumption University of Windsor" + { + "type": "website", + "value": "https://galgotiacollege.edu" + }, + { + "type": "wikipedia", + "value": "https://en.wikipedia.org/wiki/Galgotias_College" + } ], - "acronyms": [ - + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "IN", + "country_name": "India", + "country_subdivision_code": "UP", + "country_subdivision_name": "Uttar Pradesh", + "lat": 28.49615, + "lng": 77.53601, + "name": "Greater Noida" + }, + "geonames_id": 6954929 + } ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Windsor", - "labels": [ + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "GCET" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Galgotias College of Engineering & Technology" + }, { - "label": "Université de windsor", - "iso639": "fr" + "lang": "en", + "types": [ + "alias" + ], + "value": "Galgotias College of Engineering and Technology" } ], - "country": { - "country_name": "Canada", - "country_code": "CA" + "relationships": [], + "status": "active", + "types": [ + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "eli-beams.eu" + ], + "established": 2015, + "external_ids": [ + { "all": [ - "0000 0004 1936 9596" - ] + "grid.494603.c" + ], + "preferred": "grid.494603.c", + "type": "grid" }, - "FundRef": { - "preferred": "100009154", + { "all": [ - "100009154", - "501100000083" - ] + "0000 0004 7422 3856" + ], + "preferred": "0000 0004 7422 3856", + "type": "isni" }, - "OrgRef": { - "preferred": null, + { "all": [ - "342733" - ] - }, - "Wikidata": { + "Q39039051" + ], "preferred": null, - "all": [ - "Q2065769" - ] - }, - "GRID": { - "preferred": "grid.267455.7", - "all": "grid.267455.7" + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/02gdzyx04", - "name": "University of Winnipeg", - "email_address": null, - "ip_addresses": [ - ], - "established": 1871, - "types": [ - "Education" + "id": "https://ror.org/00yzpcc69", + "links": [ + { + "type": "website", + "value": "https://www.eli-beams.eu" + } ], - "relationships": [ + "locations": [ { - "label": "Winnipeg Institute for Theoretical Physics", - "type": "Child", - "id": "https://ror.org/010tw2j24" - } - ], - "addresses": [ - { - "lat": 49.890122, - "lng": -97.153367, - "state": "Manitoba", - "state_code": "CA-MB", - "city": "Winnipeg", - "geonames_city": { - "id": 6183235, - "city": "Winnipeg", - "geonames_admin1": { - "name": "Manitoba", - "id": 6065171, - "ascii_name": "Manitoba", - "code": "CA.03" - }, - "geonames_admin2": { - "name": null, - "id": null, - "ascii_name": null, - "code": null - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "CZ", + "country_name": "Czechia", + "country_subdivision_code": "20", + "country_subdivision_name": "Central Bohemia", + "lat": 49.96321, + "lng": 14.4585, + "name": "Dolní Břežany" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6251999 + "geonames_id": 3076915 } ], - "links": [ - "http://www.uwinnipeg.ca/" - ], - "aliases": [ - - ], - "acronyms": [ - - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Winnipeg", - "labels": [ + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "ELI-BL" + }, { - "label": "Université de winnipeg", - "iso639": "fr" + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Extreme Light Infrastructure Beamlines" } ], - "country": { - "country_name": "Canada", - "country_code": "CA" - }, - "external_ids": { - "ISNI": { - "preferred": null, - "all": [ - "0000 0001 1703 4731" - ] + "relationships": [], + "status": "active", + "types": [ + "facility" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" }, - "FundRef": { - "preferred": null, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } + }, + "domains": [], + "established": 1972, + "external_ids": [ + { "all": [ - "100009367" - ] + "grid.430864.d" + ], + "preferred": "grid.430864.d", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "587404" - ] - }, - "Wikidata": { + "0000 0000 9018 7542" + ], "preferred": null, - "all": [ - "Q472167" - ] - }, - "GRID": { - "preferred": "grid.267457.5", - "all": "grid.267457.5" + "type": "isni" } - } - }, - { - "id": "https://ror.org/03mnm0t94", - "name": "University of Wisconsin–Eau Claire", - "email_address": "", - "ip_addresses": [ - ], - "established": 1916, - "types": [ - "Education" + "id": "https://ror.org/02437s643", + "links": [ + { + "type": "website", + "value": "https://www.uillinois.edu" + } ], - "relationships": [ + "locations": [ { - "label": "University of Wisconsin System", - "type": "Parent", - "id": "https://ror.org/03ydkyb10" - } - ], - "addresses": [ - { - "lat": 44.79895, - "lng": -91.499346, - "state": "Wisconsin", - "state_code": "US-WI", - "city": "Eau Claire", - "geonames_city": { - "id": 5251436, - "city": "Eau Claire", - "geonames_admin1": { - "name": "Wisconsin", - "id": 5279468, - "ascii_name": "Wisconsin", - "code": "US.WI" - }, - "geonames_admin2": { - "name": "Eau Claire County", - "id": 5251439, - "ascii_name": "Eau Claire County", - "code": "US.WI.035" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "NA", + "continent_name": "North America", + "country_code": "US", + "country_name": "United States", + "country_subdivision_code": "IL", + "country_subdivision_name": "Illinois", + "lat": 42.27113, + "lng": -89.094, + "name": "Rockford" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 4907959 } ], - "links": [ - "http://www.uwec.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "UWEC" + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "UICOMR" + }, + { + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "University of Illinois Chicago, Rockford campus" + }, + { + "lang": "en", + "types": [ + "alias" + ], + "value": "University of Illinois at Rockford" + } ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Eau_Claire", - "labels": [ + "relationships": [ + { + "label": "University of Illinois Chicago", + "type": "parent", + "id": "https://ror.org/02mpq6x41" + }, { - "label": "Université du Wisconsin à Eau Claire", - "iso639": "fr" + "label": "Swedish American Hospital", + "type": "related", + "id": "https://ror.org/05scd7d31" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "status": "active", + "types": [ + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "chatham.edu" + ], + "established": 1869, + "external_ids": [ + { "all": [ - "0000 0001 2227 2494" - ] + "grid.411264.4" + ], + "preferred": "grid.411264.4", + "type": "grid" }, - "FundRef": { - "preferred": null, + { "all": [ - "100010315" - ] - }, - "OrgRef": { + "0000 0000 9776 1631" + ], "preferred": null, - "all": [ - "496729" - ] + "type": "isni" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q3551771" - ] - }, - "GRID": { - "preferred": "grid.267460.1", - "all": "grid.267460.1" + "Q5087708" + ], + "preferred": null, + "type": "wikidata" } - } - }, - { - "id": "https://ror.org/05hbexn54", - "name": "University of Wisconsin–Green Bay", - "email_address": null, - "ip_addresses": [ - ], - "established": 1965, - "types": [ - "Education" + "id": "https://ror.org/05n2dnq32", + "links": [ + { + "type": "website", + "value": "https://www.chatham.edu" + }, + { + "type": "wikipedia", + "value": "http://en.wikipedia.org/wiki/Chatham_University" + } ], - "relationships": [ + "locations": [ { - "label": "University of Wisconsin System", - "type": "Parent", - "id": "https://ror.org/03ydkyb10" - } - ], - "addresses": [ - { - "lat": 44.533203, - "lng": -87.921521, - "state": "Wisconsin", - "state_code": "US-WI", - "city": "Green Bay", - "geonames_city": { - "id": 5254962, - "city": "Green Bay", - "geonames_admin1": { - "name": "Wisconsin", - "id": 5279468, - "ascii_name": "Wisconsin", - "code": "US.WI" - }, - "geonames_admin2": { - "name": "Brown County", - "id": 5246898, - "ascii_name": "Brown County", - "code": "US.WI.009" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "NA", + "continent_name": "North America", + "country_code": "US", + "country_name": "United States", + "country_subdivision_code": "PA", + "country_subdivision_name": "Pennsylvania", + "lat": 40.44062, + "lng": -79.99589, + "name": "Pittsburgh" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 5206379 } ], - "links": [ - "http://www.uwgb.edu/" - ], - "aliases": [ - - ], - "acronyms": [ - "UWGB" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Green_Bay", - "labels": [ + "names": [ { - "label": "Université du Wisconsin–Green Bay", - "iso639": "fr" + "lang": "en", + "types": [ + "ror_display", + "label" + ], + "value": "Chatham University" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "relationships": [], + "status": "active", + "types": [ + "education" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "lds.no" + ], + "established": 1992, + "external_ids": [ + { "all": [ - "0000 0001 0559 7692" - ] + "501100010678" + ], + "preferred": "501100010678", + "type": "fundref" }, - "OrgRef": { - "preferred": null, + { "all": [ - "1513886" - ] + "grid.416137.6" + ], + "preferred": "grid.416137.6", + "type": "grid" }, - "Wikidata": { - "preferred": null, + { "all": [ - "Q2378091" - ] - }, - "GRID": { - "preferred": "grid.267461.0", - "all": "grid.267461.0" + "0000 0004 0627 3157" + ], + "preferred": null, + "type": "isni" } - } - }, - { - "id": "https://ror.org/00x8ccz20", - "name": "University of Wisconsin–La Crosse", - "email_address": "", - "ip_addresses": [ - ], - "established": 1909, - "types": [ - "Education" + "id": "https://ror.org/03ym7ve89", + "links": [ + { + "type": "website", + "value": "https://www.lovisenbergsykehus.no" + } ], - "relationships": [ + "locations": [ { - "label": "University of Wisconsin System", - "type": "Parent", - "id": "https://ror.org/03ydkyb10" - } - ], - "addresses": [ - { - "lat": 43.815576, - "lng": -91.233517, - "state": "Wisconsin", - "state_code": "US-WI", - "city": "La Crosse", - "geonames_city": { - "id": 5258957, - "city": "La Crosse", - "geonames_admin1": { - "name": "Wisconsin", - "id": 5279468, - "ascii_name": "Wisconsin", - "code": "US.WI" - }, - "geonames_admin2": { - "name": "La Crosse County", - "id": 5258961, - "ascii_name": "La Crosse County", - "code": "US.WI.063" - }, - "license": { - "attribution": "Data from geonames.org under a CC-BY 4.0 license", - "license": "https://creativecommons.org/licenses/by/4.0/" - }, - "nuts_level1": { - "name": null, - "code": null - }, - "nuts_level2": { - "name": null, - "code": null - }, - "nuts_level3": { - "name": null, - "code": null - } + "geonames_details": { + "continent_code": "EU", + "continent_name": "Europe", + "country_code": "NO", + "country_name": "Norway", + "country_subdivision_code": "03", + "country_subdivision_name": "Oslo", + "lat": 59.91273, + "lng": 10.74609, + "name": "Oslo" }, - "postcode": null, - "primary": false, - "line": null, - "country_geonames_id": 6252001 + "geonames_id": 3143244 } ], - "links": [ - "http://www.uwlax.edu/Home/Future-Students/" - ], - "aliases": [ - - ], - "acronyms": [ - "UW–L" - ], - "status": "active", - "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93La_Crosse", - "labels": [ + "names": [ { - "label": "Université du Wisconsin–La Crosse", - "iso639": "fr" + "lang": "no", + "types": [ + "ror_display", + "label" + ], + "value": "Lovisenberg Diakonale Sykehus" } ], - "country": { - "country_name": "United States", - "country_code": "US" + "relationships": [], + "status": "active", + "types": [ + "funder", + "healthcare" + ] + }, + { + "admin": { + "created": { + "date": "2018-11-14", + "schema_version": "1.0" + }, + "last_modified": { + "date": "2024-12-11", + "schema_version": "2.1" + } }, - "external_ids": { - "ISNI": { - "preferred": null, + "domains": [ + "uim-makassar.ac.id" + ], + "established": 2000, + "external_ids": [ + { "all": [ - "0000 0001 2169 5137" - ] + "grid.443680.d" + ], + "preferred": "grid.443680.d", + "type": "grid" }, - "OrgRef": { - "preferred": null, + { "all": [ - "2422287" - ] - }, - "Wikidata": { + "0000 0001 0588 5299" + ], "preferred": null, + "type": "isni" + }, + { "all": [ - "Q2688358" - ] + "Q12523343" + ], + "preferred": null, + "type": "wikidata" + } + ], + "id": "https://ror.org/05baqgp89", + "links": [ + { + "type": "website", + "value": "https://uim-makassar.ac.id/" + } + ], + "locations": [ + { + "geonames_details": { + "continent_code": "AS", + "continent_name": "Asia", + "country_code": "ID", + "country_name": "Indonesia", + "country_subdivision_code": "SN", + "country_subdivision_name": "South Sulawesi", + "lat": -5.14861, + "lng": 119.43194, + "name": "Makassar" + }, + "geonames_id": 1622786 + } + ], + "names": [ + { + "lang": null, + "types": [ + "acronym" + ], + "value": "UIM" }, - "GRID": { - "preferred": "grid.267462.3", - "all": "grid.267462.3" + { + "lang": "id", + "types": [ + "ror_display", + "label" + ], + "value": "Universitas Islam Makassar" } - } + ], + "relationships": [], + "status": "active", + "types": [ + "education" + ] } ], "meta": { "types": [ { "id": "company", - "title": "Company", - "count": 29790 + "title": "company", + "count": 30791 }, { "id": "education", - "title": "Education", - "count": 20325 + "title": "education", + "count": 22599 + }, + { + "id": "funder", + "title": "funder", + "count": 17078 }, { "id": "nonprofit", - "title": "Nonprofit", - "count": 14187 + "title": "nonprofit", + "count": 15641 }, { "id": "healthcare", - "title": "Healthcare", - "count": 13107 + "title": "healthcare", + "count": 14062 }, { "id": "facility", - "title": "Facility", - "count": 10080 + "title": "facility", + "count": 12758 }, { "id": "other", - "title": "Other", - "count": 8369 + "title": "other", + "count": 9026 }, { "id": "government", - "title": "Government", - "count": 6511 + "title": "government", + "count": 7599 }, { "id": "archive", - "title": "Archive", - "count": 2967 + "title": "archive", + "count": 3104 } ], "countries": [ { "id": "us", "title": "United States", - "count": 31196 + "count": 32118 }, { "id": "gb", "title": "United Kingdom", - "count": 7410 + "count": 7581 }, { - "id": "de", - "title": "Germany", - "count": 5189 + "id": "jp", + "title": "Japan", + "count": 5754 }, { - "id": "cn", - "title": "China", - "count": 4846 + "id": "de", + "title": "Germany", + "count": 5372 }, { "id": "fr", "title": "France", - "count": 4344 + "count": 5110 }, { - "id": "jp", - "title": "Japan", - "count": 3940 + "id": "cn", + "title": "China", + "count": 5001 }, { "id": "ca", "title": "Canada", - "count": 3392 + "count": 3610 }, { "id": "in", "title": "India", - "count": 3075 + "count": 3399 }, { "id": "cz", "title": "Czech Republic", - "count": 2780 + "count": 2843 + }, + { + "id": "it", + "title": "Italy", + "count": 2196 + } + ], + "continents": [ + { + "id": "eu", + "title": "Europe", + "count": 45322 + }, + { + "id": "na", + "title": "North America", + "count": 37230 + }, + { + "id": "as", + "title": "Asia", + "count": 23498 + }, + { + "id": "af", + "title": "Africa", + "count": 3835 + }, + { + "id": "sa", + "title": "South America", + "count": 3583 + }, + { + "id": "oc", + "title": "Oceania", + "count": 1945 }, { - "id": "ru", - "title": "Russia", - "count": 2109 + "id": "an", + "title": "Antarctica", + "count": 2 } ], "statuses": [ { "id": "active", "title": "active", - "count": 105336 + "count": 115409 } ] } -} +} \ No newline at end of file From c11b4d5e6788b317213d4f1d2918f4987ba49348 Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Wed, 6 Aug 2025 17:34:37 -0300 Subject: [PATCH 296/701] increase request timeout for ROR API (cherry picked from commit 7393ce023f9cce15effb78fb1cbd7bad7d84e4f3) --- .../ror/service/RorImportMetadataSourceServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java index 8298b6d6f011..2cc6c66c7ab3 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java @@ -46,7 +46,7 @@ public class RorImportMetadataSourceServiceImpl extends AbstractImportMetadataSo private String url; - private int timeout = 1000; + private int timeout = 5000; @Autowired private LiveImportClient liveImportClient; From be9d2bd2bfbad319533fb46e7018863825856703 Mon Sep 17 00:00:00 2001 From: nwoodward Date: Wed, 18 Dec 2024 14:25:41 -0600 Subject: [PATCH 297/701] make several usage statistics parameters configurable (cherry picked from commit cd5798593417e0501ae8fe881f9c82666dc02830) --- .../app/rest/utils/UsageReportUtils.java | 38 +++++++++++++------ dspace/config/modules/usage-statistics.cfg | 19 +++++++++- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java index 4603569da84c..53f4317808a6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java @@ -27,6 +27,7 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.handle.service.HandleService; +import org.dspace.services.ConfigurationService; import org.dspace.statistics.Dataset; import org.dspace.statistics.content.DatasetDSpaceObjectGenerator; import org.dspace.statistics.content.DatasetTimeGenerator; @@ -46,6 +47,9 @@ @Component public class UsageReportUtils { + @Autowired + private ConfigurationService configurationService; + @Autowired private HandleService handleService; @@ -135,13 +139,14 @@ public UsageReportRest createUsageReport(Context context, DSpaceObject dso, Stri */ private UsageReportRest resolveGlobalUsageReport(Context context) throws SQLException, IOException, ParseException, SolrServerException { + int topItemsLimit = configurationService.getIntProperty("usage-statistics.topItemsLimit", 10); + StatisticsListing statListing = new StatisticsListing( new StatisticsDataVisits()); - // Adding a new generator for our top 10 items without a name length delimiter + // Adding a new generator for our top n items without a name length delimiter DatasetDSpaceObjectGenerator dsoAxis = new DatasetDSpaceObjectGenerator(); - // TODO make max nr of top items (views wise)? Must be set - dsoAxis.addDsoChild(Constants.ITEM, 10, false, -1); + dsoAxis.addDsoChild(Constants.ITEM, topItemsLimit, false, -1); statListing.addDatasetGenerator(dsoAxis); Dataset dataset = statListing.getDataset(context, 1); @@ -182,7 +187,7 @@ private UsageReportRest resolveTotalVisits(Context context, DSpaceObject dso) UsageReportPointDsoTotalVisitsRest totalVisitPoint = new UsageReportPointDsoTotalVisitsRest(); totalVisitPoint.setType(StringUtils.substringAfterLast(dso.getClass().getName().toLowerCase(), ".")); totalVisitPoint.setId(dso.getID().toString()); - if (dataset.getColLabels().size() > 0) { + if (!dataset.getColLabels().isEmpty()) { totalVisitPoint.setLabel(dso.getName()); totalVisitPoint.addValue("views", Integer.valueOf(dataset.getMatrix()[0][0])); } else { @@ -205,10 +210,14 @@ private UsageReportRest resolveTotalVisits(Context context, DSpaceObject dso) */ private UsageReportRest resolveTotalVisitsPerMonth(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { + String startDateInterval = + configurationService.getProperty("usage-statistics.startDateInterval", "-6"); + String endDateInterval = + configurationService.getProperty("usage-statistics.endDateInterval", "+1"); + StatisticsTable statisticsTable = new StatisticsTable(new StatisticsDataVisits(dso)); DatasetTimeGenerator timeAxis = new DatasetTimeGenerator(); - // TODO month start and end as request para? - timeAxis.setDateInterval("month", "-6", "+1"); + timeAxis.setDateInterval("month", startDateInterval, endDateInterval); statisticsTable.addDatasetGenerator(timeAxis); DatasetDSpaceObjectGenerator dsoAxis = new DatasetDSpaceObjectGenerator(); dsoAxis.addDsoChild(dso.getType(), 10, false, -1); @@ -275,7 +284,10 @@ private UsageReportRest resolveTotalDownloads(Context context, DSpaceObject dso) */ private UsageReportRest resolveTopCountries(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { - Dataset dataset = this.getTypeStatsDataset(context, dso, "countryCode", 1); + int topCountriesLimit = + configurationService.getIntProperty("usage-statistics.topCountriesLimit", 100); + + Dataset dataset = this.getTypeStatsDataset(context, dso, "countryCode", topCountriesLimit, 1); UsageReportRest usageReportRest = new UsageReportRest(); for (int i = 0; i < dataset.getColLabels().size(); i++) { @@ -299,7 +311,10 @@ private UsageReportRest resolveTopCountries(Context context, DSpaceObject dso) */ private UsageReportRest resolveTopCities(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { - Dataset dataset = this.getTypeStatsDataset(context, dso, "city", 1); + int topCitiesLimit = + configurationService.getIntProperty("usage-statistics.topCitiesLimit", 100); + + Dataset dataset = this.getTypeStatsDataset(context, dso, "city", topCitiesLimit, 1); UsageReportRest usageReportRest = new UsageReportRest(); for (int i = 0; i < dataset.getColLabels().size(); i++) { @@ -339,16 +354,17 @@ private Dataset getDSOStatsDataset(Context context, DSpaceObject dso, int facetM * @param dso DSO we want the stats dataset of * @param typeAxisString String of the type we want on the axis of the dataset (corresponds to solr field), * examples: countryCode, city + * @param typeAxisMax Maximum amount of results to return in the dataset * @param facetMinCount Minimum amount of results on a facet data point for it to be added to dataset * @return Stats dataset with the given type on the axis, of the given DSO and with given facetMinCount */ - private Dataset getTypeStatsDataset(Context context, DSpaceObject dso, String typeAxisString, int facetMinCount) + private Dataset getTypeStatsDataset(Context context, DSpaceObject dso, String typeAxisString, int typeAxisMax, + int facetMinCount) throws SQLException, IOException, ParseException, SolrServerException { StatisticsListing statListing = new StatisticsListing(new StatisticsDataVisits(dso)); DatasetTypeGenerator typeAxis = new DatasetTypeGenerator(); typeAxis.setType(typeAxisString); - // TODO make max nr of top countries/cities a request para? Must be set - typeAxis.setMax(100); + typeAxis.setMax(typeAxisMax); statListing.addDatasetGenerator(typeAxis); return statListing.getDataset(context, facetMinCount); } diff --git a/dspace/config/modules/usage-statistics.cfg b/dspace/config/modules/usage-statistics.cfg index c77bb1ca78a3..6d47a13dcfc4 100644 --- a/dspace/config/modules/usage-statistics.cfg +++ b/dspace/config/modules/usage-statistics.cfg @@ -60,4 +60,21 @@ usage-statistics.shardedByYear = false #anonymize_statistics.dns_mask = anonymized # Only anonymize statistics records older than this threshold (expressed in days) -#anonymize_statistics.time_threshold = 90 \ No newline at end of file +#anonymize_statistics.time_threshold = 90 + +# Maximum number of items to display in the usage statistics report for an entire repository +usage-statistics.topItemsLimit = 10 + +# Number of months to begin retrieving usage statistics for total visits per month of a DSpace object +# For example, -6 means include the previous six months +usage-statistics.startDateInterval = -6 + +# Number of months to end retrieving usage statistics for total visits per month of a DSpace object +# For example, +1 means include the current month +usage-statistics.endDateInterval = +1 + +# Maximum number of countries to display in the usage statistics reports +usage-statistics.topCountriesLimit = 100 + +# Maximum number of cities to display in the usage statistics reports +usage-statistics.topCitiesLimit = 100 From 11141f34ab80161cd4fffa61301beaa561adcc3b Mon Sep 17 00:00:00 2001 From: nwoodward Date: Wed, 18 Dec 2024 14:25:41 -0600 Subject: [PATCH 298/701] make several usage statistics parameters configurable (cherry picked from commit cd5798593417e0501ae8fe881f9c82666dc02830) --- .../app/rest/utils/UsageReportUtils.java | 38 +++++++++++++------ dspace/config/modules/usage-statistics.cfg | 19 +++++++++- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java index 4603569da84c..53f4317808a6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java @@ -27,6 +27,7 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.handle.service.HandleService; +import org.dspace.services.ConfigurationService; import org.dspace.statistics.Dataset; import org.dspace.statistics.content.DatasetDSpaceObjectGenerator; import org.dspace.statistics.content.DatasetTimeGenerator; @@ -46,6 +47,9 @@ @Component public class UsageReportUtils { + @Autowired + private ConfigurationService configurationService; + @Autowired private HandleService handleService; @@ -135,13 +139,14 @@ public UsageReportRest createUsageReport(Context context, DSpaceObject dso, Stri */ private UsageReportRest resolveGlobalUsageReport(Context context) throws SQLException, IOException, ParseException, SolrServerException { + int topItemsLimit = configurationService.getIntProperty("usage-statistics.topItemsLimit", 10); + StatisticsListing statListing = new StatisticsListing( new StatisticsDataVisits()); - // Adding a new generator for our top 10 items without a name length delimiter + // Adding a new generator for our top n items without a name length delimiter DatasetDSpaceObjectGenerator dsoAxis = new DatasetDSpaceObjectGenerator(); - // TODO make max nr of top items (views wise)? Must be set - dsoAxis.addDsoChild(Constants.ITEM, 10, false, -1); + dsoAxis.addDsoChild(Constants.ITEM, topItemsLimit, false, -1); statListing.addDatasetGenerator(dsoAxis); Dataset dataset = statListing.getDataset(context, 1); @@ -182,7 +187,7 @@ private UsageReportRest resolveTotalVisits(Context context, DSpaceObject dso) UsageReportPointDsoTotalVisitsRest totalVisitPoint = new UsageReportPointDsoTotalVisitsRest(); totalVisitPoint.setType(StringUtils.substringAfterLast(dso.getClass().getName().toLowerCase(), ".")); totalVisitPoint.setId(dso.getID().toString()); - if (dataset.getColLabels().size() > 0) { + if (!dataset.getColLabels().isEmpty()) { totalVisitPoint.setLabel(dso.getName()); totalVisitPoint.addValue("views", Integer.valueOf(dataset.getMatrix()[0][0])); } else { @@ -205,10 +210,14 @@ private UsageReportRest resolveTotalVisits(Context context, DSpaceObject dso) */ private UsageReportRest resolveTotalVisitsPerMonth(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { + String startDateInterval = + configurationService.getProperty("usage-statistics.startDateInterval", "-6"); + String endDateInterval = + configurationService.getProperty("usage-statistics.endDateInterval", "+1"); + StatisticsTable statisticsTable = new StatisticsTable(new StatisticsDataVisits(dso)); DatasetTimeGenerator timeAxis = new DatasetTimeGenerator(); - // TODO month start and end as request para? - timeAxis.setDateInterval("month", "-6", "+1"); + timeAxis.setDateInterval("month", startDateInterval, endDateInterval); statisticsTable.addDatasetGenerator(timeAxis); DatasetDSpaceObjectGenerator dsoAxis = new DatasetDSpaceObjectGenerator(); dsoAxis.addDsoChild(dso.getType(), 10, false, -1); @@ -275,7 +284,10 @@ private UsageReportRest resolveTotalDownloads(Context context, DSpaceObject dso) */ private UsageReportRest resolveTopCountries(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { - Dataset dataset = this.getTypeStatsDataset(context, dso, "countryCode", 1); + int topCountriesLimit = + configurationService.getIntProperty("usage-statistics.topCountriesLimit", 100); + + Dataset dataset = this.getTypeStatsDataset(context, dso, "countryCode", topCountriesLimit, 1); UsageReportRest usageReportRest = new UsageReportRest(); for (int i = 0; i < dataset.getColLabels().size(); i++) { @@ -299,7 +311,10 @@ private UsageReportRest resolveTopCountries(Context context, DSpaceObject dso) */ private UsageReportRest resolveTopCities(Context context, DSpaceObject dso) throws SQLException, IOException, ParseException, SolrServerException { - Dataset dataset = this.getTypeStatsDataset(context, dso, "city", 1); + int topCitiesLimit = + configurationService.getIntProperty("usage-statistics.topCitiesLimit", 100); + + Dataset dataset = this.getTypeStatsDataset(context, dso, "city", topCitiesLimit, 1); UsageReportRest usageReportRest = new UsageReportRest(); for (int i = 0; i < dataset.getColLabels().size(); i++) { @@ -339,16 +354,17 @@ private Dataset getDSOStatsDataset(Context context, DSpaceObject dso, int facetM * @param dso DSO we want the stats dataset of * @param typeAxisString String of the type we want on the axis of the dataset (corresponds to solr field), * examples: countryCode, city + * @param typeAxisMax Maximum amount of results to return in the dataset * @param facetMinCount Minimum amount of results on a facet data point for it to be added to dataset * @return Stats dataset with the given type on the axis, of the given DSO and with given facetMinCount */ - private Dataset getTypeStatsDataset(Context context, DSpaceObject dso, String typeAxisString, int facetMinCount) + private Dataset getTypeStatsDataset(Context context, DSpaceObject dso, String typeAxisString, int typeAxisMax, + int facetMinCount) throws SQLException, IOException, ParseException, SolrServerException { StatisticsListing statListing = new StatisticsListing(new StatisticsDataVisits(dso)); DatasetTypeGenerator typeAxis = new DatasetTypeGenerator(); typeAxis.setType(typeAxisString); - // TODO make max nr of top countries/cities a request para? Must be set - typeAxis.setMax(100); + typeAxis.setMax(typeAxisMax); statListing.addDatasetGenerator(typeAxis); return statListing.getDataset(context, facetMinCount); } diff --git a/dspace/config/modules/usage-statistics.cfg b/dspace/config/modules/usage-statistics.cfg index c77bb1ca78a3..6d47a13dcfc4 100644 --- a/dspace/config/modules/usage-statistics.cfg +++ b/dspace/config/modules/usage-statistics.cfg @@ -60,4 +60,21 @@ usage-statistics.shardedByYear = false #anonymize_statistics.dns_mask = anonymized # Only anonymize statistics records older than this threshold (expressed in days) -#anonymize_statistics.time_threshold = 90 \ No newline at end of file +#anonymize_statistics.time_threshold = 90 + +# Maximum number of items to display in the usage statistics report for an entire repository +usage-statistics.topItemsLimit = 10 + +# Number of months to begin retrieving usage statistics for total visits per month of a DSpace object +# For example, -6 means include the previous six months +usage-statistics.startDateInterval = -6 + +# Number of months to end retrieving usage statistics for total visits per month of a DSpace object +# For example, +1 means include the current month +usage-statistics.endDateInterval = +1 + +# Maximum number of countries to display in the usage statistics reports +usage-statistics.topCountriesLimit = 100 + +# Maximum number of cities to display in the usage statistics reports +usage-statistics.topCitiesLimit = 100 From 5ed0b28a364cc20e063a3e2866a2d77796cbb905 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 3 Sep 2025 11:35:05 +0200 Subject: [PATCH 299/701] 133552: AIP packager bitstream url fix (cherry picked from commit 11da562ea40cf61790ee755a0069955a7a26586e) --- .../content/crosswalk/PREMISCrosswalk.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java index 39b6c8f29c80..1d38249e6b55 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java @@ -224,29 +224,17 @@ public Element disseminateElement(Context context, DSpaceObject dso) // c. made-up name based on sequence ID and extension. String sid = String.valueOf(bitstream.getSequenceID()); String baseUrl = configurationService.getProperty("dspace.ui.url"); - String handle = null; - // get handle of parent Item of this bitstream, if there is one: - List bn = bitstream.getBundles(); - if (bn.size() > 0) { - List bi = bn.get(0).getItems(); - if (bi.size() > 0) { - handle = bi.get(0).getHandle(); - } - } // get or make up name for bitstream: String bsName = bitstream.getName(); if (bsName == null) { List ext = bitstream.getFormat(context).getExtensions(); bsName = "bitstream_" + sid + (ext.size() > 0 ? ext.get(0) : ""); } - if (handle != null && baseUrl != null) { + if (baseUrl != null) { oiv.setText(baseUrl - + "/bitstream/" - + URLEncoder.encode(handle, "UTF-8") - + "/" - + sid - + "/" - + URLEncoder.encode(bsName, "UTF-8")); + + "/bitstreams/" + + bitstream.getID() + + "/download"); } else { oiv.setText(URLEncoder.encode(bsName, "UTF-8")); } From cbecc64a5a5928f4f4885db17b42899f05c17714 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 3 Sep 2025 11:56:42 +0200 Subject: [PATCH 300/701] 133552: unused import (cherry picked from commit 93240941d32b7a28533a666907d50387a7a1148a) --- .../main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java index 1d38249e6b55..6b6c0fd7c5a4 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java @@ -20,9 +20,7 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; -import org.dspace.content.Bundle; import org.dspace.content.DSpaceObject; -import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.BitstreamFormatService; import org.dspace.content.service.BitstreamService; From 9674bc05be5742ce19916904a71fd6f68e0fdba4 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 3 Sep 2025 11:35:05 +0200 Subject: [PATCH 301/701] 133552: AIP packager bitstream url fix (cherry picked from commit 11da562ea40cf61790ee755a0069955a7a26586e) --- .../content/crosswalk/PREMISCrosswalk.java | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java index 39b6c8f29c80..1d38249e6b55 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java @@ -224,29 +224,17 @@ public Element disseminateElement(Context context, DSpaceObject dso) // c. made-up name based on sequence ID and extension. String sid = String.valueOf(bitstream.getSequenceID()); String baseUrl = configurationService.getProperty("dspace.ui.url"); - String handle = null; - // get handle of parent Item of this bitstream, if there is one: - List bn = bitstream.getBundles(); - if (bn.size() > 0) { - List bi = bn.get(0).getItems(); - if (bi.size() > 0) { - handle = bi.get(0).getHandle(); - } - } // get or make up name for bitstream: String bsName = bitstream.getName(); if (bsName == null) { List ext = bitstream.getFormat(context).getExtensions(); bsName = "bitstream_" + sid + (ext.size() > 0 ? ext.get(0) : ""); } - if (handle != null && baseUrl != null) { + if (baseUrl != null) { oiv.setText(baseUrl - + "/bitstream/" - + URLEncoder.encode(handle, "UTF-8") - + "/" - + sid - + "/" - + URLEncoder.encode(bsName, "UTF-8")); + + "/bitstreams/" + + bitstream.getID() + + "/download"); } else { oiv.setText(URLEncoder.encode(bsName, "UTF-8")); } From 6dff0b5221c3cef1106136b182371c9243e82002 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 3 Sep 2025 11:56:42 +0200 Subject: [PATCH 302/701] 133552: unused import (cherry picked from commit 93240941d32b7a28533a666907d50387a7a1148a) --- .../main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java index 1d38249e6b55..6b6c0fd7c5a4 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/PREMISCrosswalk.java @@ -20,9 +20,7 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; -import org.dspace.content.Bundle; import org.dspace.content.DSpaceObject; -import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.BitstreamFormatService; import org.dspace.content.service.BitstreamService; From d6c3ae1897f96aa1147fb607274027f2935ec834 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:10:46 -0300 Subject: [PATCH 303/701] fix(#11191): Align Content-Disposition with RFC 5987/6266 --- .../rest/utils/HttpHeadersInitializer.java | 54 +++++++++++++++++-- .../app/rest/BitstreamRestControllerIT.java | 11 ++-- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java index d1b80c36750b..2e9bc260f49f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java @@ -7,11 +7,13 @@ */ package org.dspace.app.rest.utils; -import static jakarta.mail.internet.MimeUtility.encodeText; import static java.util.Objects.isNull; import static java.util.Objects.nonNull; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; import java.util.Arrays; import java.util.Collections; import java.util.Objects; @@ -171,9 +173,16 @@ public HttpHeaders initialiseHeaders() throws IOException { // distposition may be null here if contentType is null if (!isNullOrEmpty(disposition)) { - httpHeaders.put(CONTENT_DISPOSITION, Collections.singletonList(String.format(CONTENT_DISPOSITION_FORMAT, - disposition, - encodeText(fileName)))); + String fallbackAsciiName = createFallbackAsciiName(this.fileName); + String encodedUtf8Name = createEncodedUtf8Name(this.fileName); + + String headerValue = String.format( + "%s; filename=\"%s\"; filename*=UTF-8''%s", + disposition, + fallbackAsciiName, + encodedUtf8Name + ); + httpHeaders.put(CONTENT_DISPOSITION, Collections.singletonList(headerValue)); } log.debug("Content-Disposition : {}", disposition); @@ -261,4 +270,41 @@ private static boolean matches(String matchHeader, String toMatch) { return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; } + /** + * Creates a safe ASCII-only fallback filename by removing diacritics (accents) + * and replacing any remaining non-ASCII characters. + * E.g., "ä-ö-é.pdf" becomes "a-o-e.pdf". + * @param originalFilename The original filename. + * @return A string containing only ASCII characters. + */ + private String createFallbackAsciiName(String originalFilename) { + if (originalFilename == null) { + return ""; + } + String normalized = Normalizer.normalize(originalFilename, Normalizer.Form.NFD); + String withoutAccents = normalized.replaceAll("\\p{InCombiningDiacriticalMarks}+", ""); + return withoutAccents.replaceAll("[^\\x00-\\x7F]", ""); + } + + /** + * Creates a percent-encoded UTF-8 filename according to RFC 5987. + * This is for the `filename*` parameter. + * E.g., "ä ö é.pdf" becomes "%C3%A4%20%C3%B6%20%C3%A9.pdf". + * @param originalFilename The original filename. + * @return A percent-encoded string. + */ + private String createEncodedUtf8Name(String originalFilename) { + if (originalFilename == null) { + return ""; + } + try { + String encoded = URLEncoder.encode(originalFilename, StandardCharsets.UTF_8.toString()); + return encoded.replace("+", "%20"); + } catch (java.io.UnsupportedEncodingException e) { + // Fallback to a simple ASCII name if encoding fails. + log.error("UTF-8 encoding not supported, which should not happen.", e); + return createFallbackAsciiName(originalFilename); + } + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 691927c6e457..596e4e0c6b70 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -7,7 +7,6 @@ */ package org.dspace.app.rest; -import static jakarta.mail.internet.MimeUtility.encodeText; import static java.util.UUID.randomUUID; import static org.apache.commons.codec.CharEncoding.UTF_8; import static org.apache.commons.collections.CollectionUtils.isEmpty; @@ -348,7 +347,11 @@ public void testBitstreamName() throws Exception { //2. A public item with a bitstream String bitstreamContent = "0123456789"; - String bitstreamName = "ภาษาไทย"; + String bitstreamName = "ภาษาไทย-com-acentuação.pdf"; + String expectedAscii = "-com-acentuacao.pdf"; + String expectedUtf8Encoded = + "%E0%B8%A0%E0%B8%B2%E0%B8%A9%E0%B8%B2%E0%B9%84%E0%B8%97%E0%B8%A2-" + + "com-acentua%C3%A7%C3%A3o.pdf"; try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { @@ -372,7 +375,9 @@ public void testBitstreamName() throws Exception { //We expect the content disposition to have the encoded bitstream name .andExpect(header().string( "Content-Disposition", - "attachment;filename=\"" + encodeText(bitstreamName) + "\"" + String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s", + expectedAscii, + expectedUtf8Encoded) )); } From fe4077acee30fac8fb5607821a7d6e1ee3bd9d88 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:10:46 -0300 Subject: [PATCH 304/701] fix(#11191): Align Content-Disposition with RFC 5987/6266 --- .../rest/utils/HttpHeadersInitializer.java | 54 +++++++++++++++++-- .../app/rest/BitstreamRestControllerIT.java | 11 ++-- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java index d68c710a3c7a..67ad7202b5a9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java @@ -9,9 +9,11 @@ import static java.util.Objects.isNull; import static java.util.Objects.nonNull; -import static javax.mail.internet.MimeUtility.encodeText; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.Normalizer; import java.util.Arrays; import java.util.Collections; import java.util.Objects; @@ -171,9 +173,16 @@ public HttpHeaders initialiseHeaders() throws IOException { // distposition may be null here if contentType is null if (!isNullOrEmpty(disposition)) { - httpHeaders.put(CONTENT_DISPOSITION, Collections.singletonList(String.format(CONTENT_DISPOSITION_FORMAT, - disposition, - encodeText(fileName)))); + String fallbackAsciiName = createFallbackAsciiName(this.fileName); + String encodedUtf8Name = createEncodedUtf8Name(this.fileName); + + String headerValue = String.format( + "%s; filename=\"%s\"; filename*=UTF-8''%s", + disposition, + fallbackAsciiName, + encodedUtf8Name + ); + httpHeaders.put(CONTENT_DISPOSITION, Collections.singletonList(headerValue)); } log.debug("Content-Disposition : {}", disposition); @@ -261,4 +270,41 @@ private static boolean matches(String matchHeader, String toMatch) { return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; } + /** + * Creates a safe ASCII-only fallback filename by removing diacritics (accents) + * and replacing any remaining non-ASCII characters. + * E.g., "ä-ö-é.pdf" becomes "a-o-e.pdf". + * @param originalFilename The original filename. + * @return A string containing only ASCII characters. + */ + private String createFallbackAsciiName(String originalFilename) { + if (originalFilename == null) { + return ""; + } + String normalized = Normalizer.normalize(originalFilename, Normalizer.Form.NFD); + String withoutAccents = normalized.replaceAll("\\p{InCombiningDiacriticalMarks}+", ""); + return withoutAccents.replaceAll("[^\\x00-\\x7F]", ""); + } + + /** + * Creates a percent-encoded UTF-8 filename according to RFC 5987. + * This is for the `filename*` parameter. + * E.g., "ä ö é.pdf" becomes "%C3%A4%20%C3%B6%20%C3%A9.pdf". + * @param originalFilename The original filename. + * @return A percent-encoded string. + */ + private String createEncodedUtf8Name(String originalFilename) { + if (originalFilename == null) { + return ""; + } + try { + String encoded = URLEncoder.encode(originalFilename, StandardCharsets.UTF_8.toString()); + return encoded.replace("+", "%20"); + } catch (java.io.UnsupportedEncodingException e) { + // Fallback to a simple ASCII name if encoding fails. + log.error("UTF-8 encoding not supported, which should not happen.", e); + return createFallbackAsciiName(originalFilename); + } + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 45f8565fbaf3..095d4d89fb1f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -8,7 +8,6 @@ package org.dspace.app.rest; import static java.util.UUID.randomUUID; -import static javax.mail.internet.MimeUtility.encodeText; import static org.apache.commons.codec.CharEncoding.UTF_8; import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.io.IOUtils.toInputStream; @@ -347,7 +346,11 @@ public void testBitstreamName() throws Exception { //2. A public item with a bitstream String bitstreamContent = "0123456789"; - String bitstreamName = "ภาษาไทย"; + String bitstreamName = "ภาษาไทย-com-acentuação.pdf"; + String expectedAscii = "-com-acentuacao.pdf"; + String expectedUtf8Encoded = + "%E0%B8%A0%E0%B8%B2%E0%B8%A9%E0%B8%B2%E0%B9%84%E0%B8%97%E0%B8%A2-" + + "com-acentua%C3%A7%C3%A3o.pdf"; try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { @@ -371,7 +374,9 @@ public void testBitstreamName() throws Exception { //We expect the content disposition to have the encoded bitstream name .andExpect(header().string( "Content-Disposition", - "attachment;filename=\"" + encodeText(bitstreamName) + "\"" + String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s", + expectedAscii, + expectedUtf8Encoded) )); } From 3ea0befd28848c834852207edff06fec03ccad58 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Thu, 14 Aug 2025 12:02:08 +0200 Subject: [PATCH 305/701] [CST-21947] fix security fix --- .../org/dspace/storage/bitstore/DSBitStoreService.java | 7 ++++++- dspace/config/modules/assetstore.cfg | 9 ++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java index 7743b93ca4ba..fd4e23b82581 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java @@ -19,9 +19,11 @@ import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.content.Bitstream; import org.dspace.core.Utils; +import org.dspace.services.factory.DSpaceServicesFactory; /** * Native DSpace (or "Directory Scatter" if you prefer) asset store. @@ -252,7 +254,10 @@ protected File getFile(Bitstream bitstream) throws IOException { } File bitstreamFile = new File(bufFilename.toString()); Path normalizedPath = bitstreamFile.toPath().normalize(); - if (!normalizedPath.startsWith(baseDir.getAbsolutePath())) { + String[] allowedAssetstoreRoots = DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("assetstore.allowed.roots", new String[]{}); + if (!normalizedPath.startsWith(baseDir.getAbsolutePath()) + && !StringUtils.startsWithAny(normalizedPath.toString(), allowedAssetstoreRoots)) { log.error("Bitstream path outside of assetstore root requested:" + "bitstream={}, path={}, assetstore={}", bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); diff --git a/dspace/config/modules/assetstore.cfg b/dspace/config/modules/assetstore.cfg index cbee6bd2c3a4..5e625e0c8a1a 100644 --- a/dspace/config/modules/assetstore.cfg +++ b/dspace/config/modules/assetstore.cfg @@ -12,12 +12,15 @@ assetstore.dir = ${dspace.dir}/assetstore # This value will be used as `incoming` default store inside the `bitstore.xml` # Possible values are: # - 0: to use the `localStore`; -# - 1: to use the `s3Store`. +# - 1: to use the `s3Store`. # If you want to add additional assetstores, they must be added to that bitstore.xml # and new values should be provided as key-value pairs in the `stores` map of the -# `bitstore.xml` configuration. +# `bitstore.xml` configuration. assetstore.index.primary = 0 +#if the assetstore path is symbolic link, use this configuration to allow that path. +#assetstore.allowed.roots = /data/assetstore + #---------------------------------------------------------------# #-------------- Amazon S3 Specific Configurations --------------# #---------------------------------------------------------------# @@ -54,4 +57,4 @@ assetstore.s3.awsSecretKey = # If the credentials are left empty, # then this setting is ignored and the default AWS region will be used. -assetstore.s3.awsRegionName = \ No newline at end of file +assetstore.s3.awsRegionName = From 7dd2b5318b4f00e8dc17d6f82bfb4ccacc4bcc77 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Thu, 14 Aug 2025 12:02:08 +0200 Subject: [PATCH 306/701] [CST-21947] fix security fix --- .../org/dspace/storage/bitstore/DSBitStoreService.java | 7 ++++++- dspace/config/modules/assetstore.cfg | 9 ++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java index 7743b93ca4ba..fd4e23b82581 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java @@ -19,9 +19,11 @@ import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.content.Bitstream; import org.dspace.core.Utils; +import org.dspace.services.factory.DSpaceServicesFactory; /** * Native DSpace (or "Directory Scatter" if you prefer) asset store. @@ -252,7 +254,10 @@ protected File getFile(Bitstream bitstream) throws IOException { } File bitstreamFile = new File(bufFilename.toString()); Path normalizedPath = bitstreamFile.toPath().normalize(); - if (!normalizedPath.startsWith(baseDir.getAbsolutePath())) { + String[] allowedAssetstoreRoots = DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("assetstore.allowed.roots", new String[]{}); + if (!normalizedPath.startsWith(baseDir.getAbsolutePath()) + && !StringUtils.startsWithAny(normalizedPath.toString(), allowedAssetstoreRoots)) { log.error("Bitstream path outside of assetstore root requested:" + "bitstream={}, path={}, assetstore={}", bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); diff --git a/dspace/config/modules/assetstore.cfg b/dspace/config/modules/assetstore.cfg index cbee6bd2c3a4..5e625e0c8a1a 100644 --- a/dspace/config/modules/assetstore.cfg +++ b/dspace/config/modules/assetstore.cfg @@ -12,12 +12,15 @@ assetstore.dir = ${dspace.dir}/assetstore # This value will be used as `incoming` default store inside the `bitstore.xml` # Possible values are: # - 0: to use the `localStore`; -# - 1: to use the `s3Store`. +# - 1: to use the `s3Store`. # If you want to add additional assetstores, they must be added to that bitstore.xml # and new values should be provided as key-value pairs in the `stores` map of the -# `bitstore.xml` configuration. +# `bitstore.xml` configuration. assetstore.index.primary = 0 +#if the assetstore path is symbolic link, use this configuration to allow that path. +#assetstore.allowed.roots = /data/assetstore + #---------------------------------------------------------------# #-------------- Amazon S3 Specific Configurations --------------# #---------------------------------------------------------------# @@ -54,4 +57,4 @@ assetstore.s3.awsSecretKey = # If the credentials are left empty, # then this setting is ignored and the default AWS region will be used. -assetstore.s3.awsRegionName = \ No newline at end of file +assetstore.s3.awsRegionName = From 98e89caa3586cec75e9fb5ddfb0d3ef295c126a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:33:03 +0000 Subject: [PATCH 307/701] Bump the test-tools group with 6 updates Bumps the test-tools group with 6 updates: | Package | From | To | | --- | --- | --- | | [io.netty:netty-buffer](https://github.com/netty/netty) | `4.2.4.Final` | `4.2.6.Final` | | [io.netty:netty-transport](https://github.com/netty/netty) | `4.2.4.Final` | `4.2.6.Final` | | [io.netty:netty-transport-native-unix-common](https://github.com/netty/netty) | `4.2.4.Final` | `4.2.6.Final` | | [io.netty:netty-common](https://github.com/netty/netty) | `4.2.4.Final` | `4.2.6.Final` | | [io.netty:netty-handler](https://github.com/netty/netty) | `4.2.4.Final` | `4.2.6.Final` | | [io.netty:netty-codec](https://github.com/netty/netty) | `4.2.4.Final` | `4.2.6.Final` | Updates `io.netty:netty-buffer` from 4.2.4.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.4.Final...netty-4.2.6.Final) Updates `io.netty:netty-transport` from 4.2.4.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.4.Final...netty-4.2.6.Final) Updates `io.netty:netty-transport-native-unix-common` from 4.2.4.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.4.Final...netty-4.2.6.Final) Updates `io.netty:netty-common` from 4.2.4.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.4.Final...netty-4.2.6.Final) Updates `io.netty:netty-handler` from 4.2.4.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.4.Final...netty-4.2.6.Final) Updates `io.netty:netty-codec` from 4.2.4.Final to 4.2.6.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.4.Final...netty-4.2.6.Final) --- updated-dependencies: - dependency-name: io.netty:netty-buffer dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport-native-unix-common dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-common dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-handler dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-codec dependency-version: 4.2.6.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index b8dbb4a6a7f1..261a0e94b99d 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -870,32 +870,32 @@ io.netty netty-buffer - 4.2.4.Final + 4.2.6.Final io.netty netty-transport - 4.2.4.Final + 4.2.6.Final io.netty netty-transport-native-unix-common - 4.2.4.Final + 4.2.6.Final io.netty netty-common - 4.2.4.Final + 4.2.6.Final io.netty netty-handler - 4.2.4.Final + 4.2.6.Final io.netty netty-codec - 4.2.4.Final + 4.2.6.Final org.apache.velocity From 127770219b401d36d5d9e2a67556a93578342bd9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:34:23 +0000 Subject: [PATCH 308/701] Bump com.github.spotbugs:spotbugs-maven-plugin from 4.9.4.0 to 4.9.4.2 Bumps [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) from 4.9.4.0 to 4.9.4.2. - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.4.0...spotbugs-maven-plugin-4.9.4.2) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a2ed63f2df69..96dc80d20f9b 100644 --- a/pom.xml +++ b/pom.xml @@ -295,7 +295,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.4.0 + 4.9.4.2 Max Low From 847904cadbf731cfbb6c8811680d6a5953f36a2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:35:21 +0000 Subject: [PATCH 309/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.788 to 1.12.791 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.788 to 1.12.791. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.788...1.12.791) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-version: 1.12.791 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index b8dbb4a6a7f1..1a1cc4466f9b 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -738,7 +738,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.788 + 1.12.791 From 21ce7ddcf0aad12d4eddac2958b0e6732330c6d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 21:04:15 +0000 Subject: [PATCH 310/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.788 to 1.12.791 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.788 to 1.12.791. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.788...1.12.791) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-version: 1.12.791 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 39944a08b593..108e5645cf31 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -717,7 +717,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.788 + 1.12.791 @@ -64,21 +62,14 @@ - - - - - - - + - - + From 7a95781068f13d59119f3545553ddc05c7e79ac6 Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 23 Sep 2025 16:25:33 +0200 Subject: [PATCH 333/701] remove ratingreviewaction in test context (cherry picked from commit ddccb342d6aa2d43af67cf2d38dc9d2b25c98313) --- .../config/spring/api/workflow-actions.xml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml index 0d074362279e..a7c725c524fe 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml @@ -23,7 +23,6 @@ - @@ -46,7 +45,6 @@ - @@ -66,21 +64,14 @@ - - - - - - - + - - + From 39f624144c3b31105d66edbe194edb66b8da3dbe Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 23 Sep 2025 16:23:57 +0200 Subject: [PATCH 334/701] remove ratingreviewaction bean in workflow-actions.xml (cherry picked from commit 770ab0ca79c3125cd1063f654644328bc6f87ed9) --- dspace/config/spring/api/workflow-actions.xml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/dspace/config/spring/api/workflow-actions.xml b/dspace/config/spring/api/workflow-actions.xml index d01f1b6b4c81..66d3a54e367b 100644 --- a/dspace/config/spring/api/workflow-actions.xml +++ b/dspace/config/spring/api/workflow-actions.xml @@ -21,7 +21,6 @@ - @@ -44,7 +43,6 @@ - @@ -64,21 +62,14 @@ - - - - - - - + - - + From 667de82136477153b179ce85dec03b60eff7dcfe Mon Sep 17 00:00:00 2001 From: Sascha Szott Date: Tue, 23 Sep 2025 16:25:33 +0200 Subject: [PATCH 335/701] remove ratingreviewaction in test context (cherry picked from commit ddccb342d6aa2d43af67cf2d38dc9d2b25c98313) --- .../config/spring/api/workflow-actions.xml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml index 0d074362279e..a7c725c524fe 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml @@ -23,7 +23,6 @@ - @@ -46,7 +45,6 @@ - @@ -66,21 +64,14 @@ - - - - - - - + - - + From e0c68eb5eaa295a57368fa31baf36f77dedc8e34 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 22 Sep 2025 16:57:20 -0500 Subject: [PATCH 336/701] Change to "mode=min" in order to minimize our cache size for Docker images (cherry picked from commit 1f8e290a1ec2b66fd217b0a6a3e533fb6adda714) --- .github/workflows/reusable-docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 0c3261da95da..a3729c27f6ee 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -164,7 +164,7 @@ jobs: # Use GitHub cache to load cached Docker images and cache the results of this build # This decreases the number of images we need to fetch from DockerHub cache-from: type=gha,scope=${{ inputs.build_id }} - cache-to: type=gha,scope=${{ inputs.build_id }},mode=max + cache-to: type=gha,scope=${{ inputs.build_id }},mode=min # Export the digest of Docker build locally - name: Export Docker build digest @@ -216,7 +216,7 @@ jobs: # Use GitHub cache to load cached Docker images and cache the results of this build # This decreases the number of images we need to fetch from DockerHub cache-from: type=gha,scope=${{ inputs.build_id }} - cache-to: type=gha,scope=${{ inputs.build_id }},mode=max + cache-to: type=gha,scope=${{ inputs.build_id }},mode=min # Export image to a local TAR file outputs: type=docker,dest=/tmp/${{ inputs.build_id }}.tar From de50420624d101b3cdbcc4c9aeb2b13b32d2b132 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 22 Sep 2025 16:57:20 -0500 Subject: [PATCH 337/701] Change to "mode=min" in order to minimize our cache size for Docker images --- .github/workflows/reusable-docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 528f5779ca96..8165fe7f8400 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -165,7 +165,7 @@ jobs: # Use GitHub cache to load cached Docker images and cache the results of this build # This decreases the number of images we need to fetch from DockerHub cache-from: type=gha,scope=${{ inputs.build_id }} - cache-to: type=gha,scope=${{ inputs.build_id }},mode=max + cache-to: type=gha,scope=${{ inputs.build_id }},mode=min # Export the digest of Docker build locally - name: Export Docker build digest @@ -217,7 +217,7 @@ jobs: # Use GitHub cache to load cached Docker images and cache the results of this build # This decreases the number of images we need to fetch from DockerHub cache-from: type=gha,scope=${{ inputs.build_id }} - cache-to: type=gha,scope=${{ inputs.build_id }},mode=max + cache-to: type=gha,scope=${{ inputs.build_id }},mode=min # Export image to a local TAR file outputs: type=docker,dest=/tmp/${{ inputs.build_id }}.tar From ef9b9898010a47922f67318daeaf4ac255d346b7 Mon Sep 17 00:00:00 2001 From: nwoodward Date: Fri, 19 Sep 2025 15:43:26 -0500 Subject: [PATCH 338/701] fix Hibernate bugs (cherry picked from commit 088463c94b108ec097ec659ecb111e27fa30b7d5) --- .../content/dao/impl/CollectionDAOImpl.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java index 841da319f0b2..b5a5919239b4 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; import jakarta.persistence.Query; import jakarta.persistence.criteria.CriteriaBuilder; @@ -19,6 +20,7 @@ import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.ResourcePolicy_; import org.dspace.content.Collection; @@ -40,6 +42,11 @@ * @author kevinvandevelde at atmire.com */ public class CollectionDAOImpl extends AbstractHibernateDSODAO implements CollectionDAO { + /** + * log4j logger + */ + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(CollectionDAOImpl.class); + protected CollectionDAOImpl() { super(); } @@ -172,14 +179,25 @@ public int countRows(Context context) throws SQLException { @SuppressWarnings("unchecked") public List> getCollectionsWithBitstreamSizesTotal(Context context) throws SQLException { - String q = "select col as collection, sum(bit.sizeBytes) as totalBytes from Item i join i.collections col " + - "join i.bundles bun join bun.bitstreams bit group by col"; + String q = "select col.id, sum(bit.sizeBytes) as totalBytes from Item i join i.collections col " + + "join i.bundles bun join bun.bitstreams bit group by col.id"; Query query = createQuery(context, q); + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + List list = query.getResultList(); List> returnList = new ArrayList<>(list.size()); for (Object[] o : list) { - returnList.add(new AbstractMap.SimpleEntry<>((Collection) o[0], (Long) o[1])); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Collection.class); + Root collectionRoot = criteriaQuery.from(Collection.class); + criteriaQuery.select(collectionRoot).where(criteriaBuilder.equal(collectionRoot.get("id"), (UUID) o[0])); + Query collectionQuery = createQuery(context, criteriaQuery); + Collection collection = (Collection) collectionQuery.getSingleResult(); + if (collection != null) { + returnList.add(new AbstractMap.SimpleEntry<>(collection, (Long) o[1])); + } else { + log.warn("Unable to find Collection with UUID: {}", o[0]); + } } return returnList; } From fdc33d709b6db25cdf526fa5a5d8c49c42a9c7bf Mon Sep 17 00:00:00 2001 From: nwoodward Date: Fri, 19 Sep 2025 15:43:44 -0500 Subject: [PATCH 339/701] fix Hibernate bug (cherry picked from commit 69c6d273224447334ac4848e63123c4af2349ae9) --- .../main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java index 25f102f6def4..66a775e39d80 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java @@ -178,7 +178,7 @@ public int countDeleted(Context context) throws SQLException { @Override public int countWithNoPolicy(Context context) throws SQLException { Query query = createQuery(context, - "SELECT count(bit.id) from Bitstream bit where bit.deleted<>true and bit.id not in" + + "SELECT count(bit.id) from Bitstream bit where bit.deleted<>true and bit not in" + " (select res.dSpaceObject from ResourcePolicy res where res.resourceTypeId = " + ":typeId )"); query.setParameter("typeId", Constants.BITSTREAM); From 23cd58280189ab86572491549562cc58de2a58b3 Mon Sep 17 00:00:00 2001 From: nwoodward Date: Fri, 19 Sep 2025 14:06:09 -0500 Subject: [PATCH 340/701] fix hibernate syntax bug (cherry picked from commit d9cbb9665569431596e980d583bb7c802b899a47) --- .../java/org/dspace/content/dao/impl/CollectionDAOImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java index befa1397a8ee..e5d05abe5d67 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java @@ -159,7 +159,7 @@ public List findAuthorizedByGroup(Context context, EPerson ePerson, @Override public List findCollectionsWithSubscribers(Context context) throws SQLException { - return list(createQuery(context, "SELECT DISTINCT c FROM Collection c JOIN Subscription s ON c.id = " + + return list(createQuery(context, "SELECT DISTINCT c FROM Collection c JOIN Subscription s ON c = " + "s.dSpaceObject")); } From f8ac04b5911276d2c941625ff59928a99feac65b Mon Sep 17 00:00:00 2001 From: nwoodward Date: Fri, 19 Sep 2025 14:06:52 -0500 Subject: [PATCH 341/701] lint fixes (cherry picked from commit b8812aad458e41b7aeed78430c06dd4e6d282d23) --- .../src/main/java/org/dspace/health/UserCheck.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/health/UserCheck.java b/dspace-api/src/main/java/org/dspace/health/UserCheck.java index 19a2a9ced355..875bb1bb0b60 100644 --- a/dspace-api/src/main/java/org/dspace/health/UserCheck.java +++ b/dspace-api/src/main/java/org/dspace/health/UserCheck.java @@ -50,26 +50,26 @@ public String run(ReportInfo ri) { info.put("Self registered", 0); for (EPerson e : epersons) { - if (e.getEmail() != null && e.getEmail().length() > 0) { + if (e.getEmail() != null && !e.getEmail().isEmpty()) { info.put("Have email", info.get("Have email") + 1); } if (e.canLogIn()) { info.put("Can log in (password)", info.get("Can log in (password)") + 1); } - if (e.getFirstName() != null && e.getFirstName().length() > 0) { + if (e.getFirstName() != null && !e.getFirstName().isEmpty()) { info.put("Have 1st name", info.get("Have 1st name") + 1); } - if (e.getLastName() != null && e.getLastName().length() > 0) { + if (e.getLastName() != null && !e.getLastName().isEmpty()) { info.put("Have 2nd name", info.get("Have 2nd name") + 1); } - if (e.getLanguage() != null && e.getLanguage().length() > 0) { + if (e.getLanguage() != null && !e.getLanguage().isEmpty()) { info.put("Have lang", info.get("Have lang") + 1); } - if (e.getNetid() != null && e.getNetid().length() > 0) { + if (e.getNetid() != null && !e.getNetid().isEmpty()) { info.put("Have netid", info.get("Have netid") + 1); } - if (e.getNetid() != null && e.getNetid().length() > 0) { + if (e.getNetid() != null && !e.getNetid().isEmpty()) { info.put("Self registered", info.get("Self registered") + 1); } } From ba41a8755ccd4d02eeb5e42150b36e6c70d716a6 Mon Sep 17 00:00:00 2001 From: nwoodward Date: Fri, 19 Sep 2025 14:06:09 -0500 Subject: [PATCH 342/701] fix hibernate syntax bug (cherry picked from commit d9cbb9665569431596e980d583bb7c802b899a47) --- .../java/org/dspace/content/dao/impl/CollectionDAOImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java index b5a5919239b4..4530e6eeda40 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java @@ -166,7 +166,7 @@ public List findAuthorizedByGroup(Context context, EPerson ePerson, @Override public List findCollectionsWithSubscribers(Context context) throws SQLException { - return list(createQuery(context, "SELECT DISTINCT c FROM Collection c JOIN Subscription s ON c.id = " + + return list(createQuery(context, "SELECT DISTINCT c FROM Collection c JOIN Subscription s ON c = " + "s.dSpaceObject")); } From 6611466826469035834f68806fbb07a472b5b5b1 Mon Sep 17 00:00:00 2001 From: nwoodward Date: Fri, 19 Sep 2025 14:06:52 -0500 Subject: [PATCH 343/701] lint fixes (cherry picked from commit b8812aad458e41b7aeed78430c06dd4e6d282d23) --- .../src/main/java/org/dspace/health/UserCheck.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/health/UserCheck.java b/dspace-api/src/main/java/org/dspace/health/UserCheck.java index 19a2a9ced355..875bb1bb0b60 100644 --- a/dspace-api/src/main/java/org/dspace/health/UserCheck.java +++ b/dspace-api/src/main/java/org/dspace/health/UserCheck.java @@ -50,26 +50,26 @@ public String run(ReportInfo ri) { info.put("Self registered", 0); for (EPerson e : epersons) { - if (e.getEmail() != null && e.getEmail().length() > 0) { + if (e.getEmail() != null && !e.getEmail().isEmpty()) { info.put("Have email", info.get("Have email") + 1); } if (e.canLogIn()) { info.put("Can log in (password)", info.get("Can log in (password)") + 1); } - if (e.getFirstName() != null && e.getFirstName().length() > 0) { + if (e.getFirstName() != null && !e.getFirstName().isEmpty()) { info.put("Have 1st name", info.get("Have 1st name") + 1); } - if (e.getLastName() != null && e.getLastName().length() > 0) { + if (e.getLastName() != null && !e.getLastName().isEmpty()) { info.put("Have 2nd name", info.get("Have 2nd name") + 1); } - if (e.getLanguage() != null && e.getLanguage().length() > 0) { + if (e.getLanguage() != null && !e.getLanguage().isEmpty()) { info.put("Have lang", info.get("Have lang") + 1); } - if (e.getNetid() != null && e.getNetid().length() > 0) { + if (e.getNetid() != null && !e.getNetid().isEmpty()) { info.put("Have netid", info.get("Have netid") + 1); } - if (e.getNetid() != null && e.getNetid().length() > 0) { + if (e.getNetid() != null && !e.getNetid().isEmpty()) { info.put("Self registered", info.get("Self registered") + 1); } } From 0246588ab572bc4b9e1401fd2d79a3469b88b91b Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:50:37 -0300 Subject: [PATCH 344/701] Fix (#9694): Change Solr dynamic field *.year to *_year (cherry picked from commit 9fc163fbdacd99c78518a5372881df1427c30275) --- dspace/solr/search/conf/schema.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/solr/search/conf/schema.xml b/dspace/solr/search/conf/schema.xml index 66f2f176d2d6..5c79519159a4 100644 --- a/dspace/solr/search/conf/schema.xml +++ b/dspace/solr/search/conf/schema.xml @@ -324,7 +324,7 @@ - + From d56126fe007e5ba31c4b4bcf34c698a17710f19c Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:50:37 -0300 Subject: [PATCH 345/701] Fix (#9694): Change Solr dynamic field *.year to *_year (cherry picked from commit 9fc163fbdacd99c78518a5372881df1427c30275) --- dspace/solr/search/conf/schema.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/solr/search/conf/schema.xml b/dspace/solr/search/conf/schema.xml index 66f2f176d2d6..5c79519159a4 100644 --- a/dspace/solr/search/conf/schema.xml +++ b/dspace/solr/search/conf/schema.xml @@ -324,7 +324,7 @@ - + From ae7e1387a6c2e2242dfe4b091a9b7c90f9d073ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:18:26 +0000 Subject: [PATCH 346/701] Bump the build-tools group with 10 updates Bumps the build-tools group with 10 updates: | Package | From | To | | --- | --- | --- | | [com.google.errorprone:error_prone_core](https://github.com/google/error-prone) | `2.41.0` | `2.42.0` | | [com.google.errorprone:error_prone_annotations](https://github.com/google/error-prone) | `2.41.0` | `2.42.0` | | [com.github.spotbugs:spotbugs](https://github.com/spotbugs/spotbugs) | `4.9.4` | `4.9.6` | | [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin) | `3.14.0` | `3.14.1` | | [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire) | `3.5.3` | `3.5.4` | | [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) | `3.5.3` | `3.5.4` | | [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) | `4.9.4.2` | `4.9.6.0` | | [org.sonatype.central:central-publishing-maven-plugin](https://github.com/sonatype/central-publishing-maven-plugin) | `0.8.0` | `0.9.0` | | [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) | `3.11.3` | `3.12.0` | | [org.codehaus.mojo:license-maven-plugin](https://github.com/mojohaus/license-maven-plugin) | `2.6.0` | `2.7.0` | Updates `com.google.errorprone:error_prone_core` from 2.41.0 to 2.42.0 - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.41.0...v2.42.0) Updates `com.google.errorprone:error_prone_annotations` from 2.41.0 to 2.42.0 - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.41.0...v2.42.0) Updates `com.github.spotbugs:spotbugs` from 4.9.4 to 4.9.6 - [Release notes](https://github.com/spotbugs/spotbugs/releases) - [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotbugs/spotbugs/compare/4.9.4...4.9.6) Updates `com.google.errorprone:error_prone_annotations` from 2.41.0 to 2.42.0 - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.41.0...v2.42.0) Updates `org.apache.maven.plugins:maven-compiler-plugin` from 3.14.0 to 3.14.1 - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.14.0...maven-compiler-plugin-3.14.1) Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.3...surefire-3.5.4) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.5.3 to 3.5.4 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.3...surefire-3.5.4) Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.9.4.2 to 4.9.6.0 - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.4.2...spotbugs-maven-plugin-4.9.6.0) Updates `org.sonatype.central:central-publishing-maven-plugin` from 0.8.0 to 0.9.0 - [Commits](https://github.com/sonatype/central-publishing-maven-plugin/commits) Updates `org.apache.maven.plugins:maven-javadoc-plugin` from 3.11.3 to 3.12.0 - [Release notes](https://github.com/apache/maven-javadoc-plugin/releases) - [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.11.3...maven-javadoc-plugin-3.12.0) Updates `org.codehaus.mojo:license-maven-plugin` from 2.6.0 to 2.7.0 - [Release notes](https://github.com/mojohaus/license-maven-plugin/releases) - [Commits](https://github.com/mojohaus/license-maven-plugin/compare/2.6.0...2.7.0) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_core dependency-version: 2.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: com.google.errorprone:error_prone_annotations dependency-version: 2.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: com.github.spotbugs:spotbugs dependency-version: 4.9.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: com.google.errorprone:error_prone_annotations dependency-version: 2.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.14.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.5.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.6.0 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.sonatype.central:central-publishing-maven-plugin dependency-version: 0.9.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-javadoc-plugin dependency-version: 3.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.codehaus.mojo:license-maven-plugin dependency-version: 2.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 5c12bb219acf..24cb4e51625f 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ 8.11.4 3.11.1 - 2.41.0 + 2.42.0 2.19.2 2.19.2 @@ -140,7 +140,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + 3.14.1 ${java.version} @@ -208,7 +208,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.3 + 3.5.4 @@ -235,7 +235,7 @@ maven-failsafe-plugin - 3.5.3 + 3.5.4 @@ -303,7 +303,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.4.2 + 4.9.6.0 Max Low @@ -313,7 +313,7 @@ com.github.spotbugs spotbugs - 4.9.4 + 4.9.6 @@ -365,13 +365,13 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.9.0 org.apache.maven.plugins maven-javadoc-plugin - 3.11.3 + 3.12.0 false @@ -693,7 +693,7 @@ org.codehaus.mojo license-maven-plugin - 2.6.0 + 2.7.0 false From 1642fa705fd26b00c3b655fb7144114ba58adeb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:18:46 +0000 Subject: [PATCH 347/701] Bump the test-tools group with 3 updates Bumps the test-tools group with 3 updates: [com.h2database:h2](https://github.com/h2database/h2database), [org.xmlunit:xmlunit-core](https://github.com/xmlunit/xmlunit) and [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client). Updates `com.h2database:h2` from 2.3.232 to 2.4.240 - [Release notes](https://github.com/h2database/h2database/releases) - [Commits](https://github.com/h2database/h2database/compare/version-2.3.232...version-2.4.240) Updates `org.xmlunit:xmlunit-core` from 2.10.3 to 2.10.4 - [Release notes](https://github.com/xmlunit/xmlunit/releases) - [Changelog](https://github.com/xmlunit/xmlunit/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/xmlunit/xmlunit/compare/v2.10.3...v2.10.4) Updates `org.apache.httpcomponents.client5:httpclient5` from 5.5 to 5.5.1 - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.5.1/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.5...rel/v5.5.1) --- updated-dependencies: - dependency-name: com.h2database:h2 dependency-version: 2.4.240 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: test-tools - dependency-name: org.xmlunit:xmlunit-core dependency-version: 2.10.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-version: 5.5.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- dspace-server-webapp/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 108e5645cf31..1aa596458823 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -771,7 +771,7 @@ org.xmlunit xmlunit-core - 2.10.3 + 2.10.4 test diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 456eaba0b374..aaf177d4d4a4 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -590,7 +590,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.5 + 5.5.1 test diff --git a/pom.xml b/pom.xml index 5c12bb219acf..8c9a3f360b56 100644 --- a/pom.xml +++ b/pom.xml @@ -1703,7 +1703,7 @@ com.h2database h2 - 2.3.232 + 2.4.240 test From efd35e3a308eb5283d0e9a3357b73d531af6a2ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:19:07 +0000 Subject: [PATCH 348/701] Bump org.apache.commons:commons-lang3 in the apache-commons group Bumps the apache-commons group with 1 update: org.apache.commons:commons-lang3. Updates `org.apache.commons:commons-lang3` from 3.18.0 to 3.19.0 --- updated-dependencies: - dependency-name: org.apache.commons:commons-lang3 dependency-version: 3.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5c12bb219acf..5cd690c5436f 100644 --- a/pom.xml +++ b/pom.xml @@ -1508,7 +1508,7 @@ org.apache.commons commons-lang3 - 3.18.0 + 3.19.0 From bb86ef931b19d4350a4f2f3ff26554e6fd978c10 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:19:33 +0000 Subject: [PATCH 349/701] Bump the jakarta group with 5 updates Bumps the jakarta group with 5 updates: | Package | From | To | | --- | --- | --- | | [jakarta.activation:jakarta.activation-api](https://github.com/jakartaee/jaf-api) | `2.1.3` | `2.1.4` | | [jakarta.mail:jakarta.mail-api](https://github.com/jakartaee/mail-api) | `2.1.4` | `2.1.5` | | org.eclipse.angus:jakarta.mail | `2.0.4` | `2.0.5` | | [jakarta.xml.bind:jakarta.xml.bind-api](https://github.com/jakartaee/jaxb-api) | `4.0.2` | `4.0.4` | | org.glassfish.jaxb:jaxb-runtime | `4.0.5` | `4.0.6` | Updates `jakarta.activation:jakarta.activation-api` from 2.1.3 to 2.1.4 - [Release notes](https://github.com/jakartaee/jaf-api/releases) - [Commits](https://github.com/jakartaee/jaf-api/compare/2.1.3...2.1.4) Updates `jakarta.mail:jakarta.mail-api` from 2.1.4 to 2.1.5 - [Release notes](https://github.com/jakartaee/mail-api/releases) - [Commits](https://github.com/jakartaee/mail-api/compare/2.1.4...2.1.5) Updates `org.eclipse.angus:jakarta.mail` from 2.0.4 to 2.0.5 Updates `jakarta.xml.bind:jakarta.xml.bind-api` from 4.0.2 to 4.0.4 - [Release notes](https://github.com/jakartaee/jaxb-api/releases) - [Commits](https://github.com/jakartaee/jaxb-api/compare/4.0.2...4.0.4) Updates `org.glassfish.jaxb:jaxb-runtime` from 4.0.5 to 4.0.6 --- updated-dependencies: - dependency-name: jakarta.activation:jakarta.activation-api dependency-version: 2.1.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta - dependency-name: jakarta.mail:jakarta.mail-api dependency-version: 2.1.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta - dependency-name: org.eclipse.angus:jakarta.mail dependency-version: 2.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta - dependency-name: jakarta.xml.bind:jakarta.xml.bind-api dependency-version: 4.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta - dependency-name: org.glassfish.jaxb:jaxb-runtime dependency-version: 4.0.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta ... Signed-off-by: dependabot[bot] --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 5c12bb219acf..c86c53199459 100644 --- a/pom.xml +++ b/pom.xml @@ -34,8 +34,8 @@ 2.19.2 2.19.2 2.1.1 - 4.0.2 - 4.0.5 + 4.0.4 + 4.0.6 1.1.1 9.4.58.v20250814 @@ -1545,7 +1545,7 @@ jakarta.activation jakarta.activation-api - 2.1.3 + 2.1.4 @@ -1557,14 +1557,14 @@ jakarta.mail jakarta.mail-api - 2.1.4 + 2.1.5 provided org.eclipse.angus jakarta.mail - 2.0.4 + 2.0.5 jakarta.servlet From 0a5acdbe25b51ca482ae1d6c5e4dff21196a1bd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:22:01 +0000 Subject: [PATCH 350/701] Bump the spring group with 25 updates Bumps the spring group with 25 updates: | Package | From | To | | --- | --- | --- | | [org.springframework:spring-orm](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-core](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-beans](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-aop](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-context](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-context-support](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-tx](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-jdbc](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-web](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-expression](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework:spring-test](https://github.com/spring-projects/spring-framework) | `6.2.10` | `6.2.11` | | [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-tomcat](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-maven-plugin](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-cache](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-thymeleaf](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-web](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-data-rest](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-security](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-aop](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-actuator](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.boot:spring-boot-starter-log4j2](https://github.com/spring-projects/spring-boot) | `3.5.5` | `3.5.6` | | [org.springframework.security:spring-security-test](https://github.com/spring-projects/spring-security) | `6.5.3` | `6.5.5` | Updates `org.springframework:spring-orm` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-core` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-beans` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-aop` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-context` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-context-support` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-tx` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-jdbc` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-web` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-webmvc` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-expression` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-test` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-core` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-beans` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-aop` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-context` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-context-support` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-tx` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-jdbc` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-web` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-webmvc` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-expression` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework:spring-test` from 6.2.10 to 6.2.11 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.10...v6.2.11) Updates `org.springframework.boot:spring-boot-starter-test` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.security:spring-security-test` from 6.5.3 to 6.5.5 - [Release notes](https://github.com/spring-projects/spring-security/releases) - [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc) - [Commits](https://github.com/spring-projects/spring-security/compare/6.5.3...6.5.5) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.5 to 3.5.6 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.5...v3.5.6) --- updated-dependencies: - dependency-name: org.springframework:spring-orm dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.security:spring-security-test dependency-version: 6.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 5c12bb219acf..1c58a04306e7 100644 --- a/pom.xml +++ b/pom.xml @@ -19,9 +19,9 @@ 17 - 6.2.10 - 3.5.5 - 6.5.3 + 6.2.11 + 3.5.6 + 6.5.5 6.4.10.Final 8.0.3.Final 42.7.7 From a26fc94ee1f7b95e668d1b714a983aa1f7ec112f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:22:22 +0000 Subject: [PATCH 351/701] Bump bouncycastle.version from 1.81 to 1.82 Bumps `bouncycastle.version` from 1.81 to 1.82. Updates `org.bouncycastle:bcpkix-jdk18on` from 1.81 to 1.82 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `org.bouncycastle:bcprov-jdk18on` from 1.81 to 1.82 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `org.bouncycastle:bcutil-jdk18on` from 1.81 to 1.82 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) --- updated-dependencies: - dependency-name: org.bouncycastle:bcpkix-jdk18on dependency-version: '1.82' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.bouncycastle:bcprov-jdk18on dependency-version: '1.82' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.bouncycastle:bcutil-jdk18on dependency-version: '1.82' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5c12bb219acf..396d8f8bb1e7 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ 2.0.17 2.9.4 - 1.81 + 1.82 8.0.1 3.1.11 From 9fddc2f23e401e17e63560df7a035fbf042c4f22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:22:46 +0000 Subject: [PATCH 352/701] Bump log4j.version from 2.25.1 to 2.25.2 Bumps `log4j.version` from 2.25.1 to 2.25.2. Updates `org.apache.logging.log4j:log4j-api` from 2.25.1 to 2.25.2 Updates `org.apache.logging.log4j:log4j-core` from 2.25.1 to 2.25.2 Updates `org.apache.logging.log4j:log4j-slf4j2-impl` from 2.25.1 to 2.25.2 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-version: 2.25.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-slf4j2-impl dependency-version: 2.25.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5c12bb219acf..b953d0689c16 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 1.1.1 9.4.58.v20250814 - 2.25.1 + 2.25.2 2.0.34 1.19.0 2.0.17 From 2c95468e2714fdb1c2152a4321f2c0de07147ab7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:23:58 +0000 Subject: [PATCH 353/701] Bump com.google.code.gson:gson from 2.13.1 to 2.13.2 Bumps [com.google.code.gson:gson](https://github.com/google/gson) from 2.13.1 to 2.13.2. - [Release notes](https://github.com/google/gson/releases) - [Changelog](https://github.com/google/gson/blob/main/CHANGELOG.md) - [Commits](https://github.com/google/gson/compare/gson-parent-2.13.1...gson-parent-2.13.2) --- updated-dependencies: - dependency-name: com.google.code.gson:gson dependency-version: 2.13.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5c12bb219acf..a39788f03c56 100644 --- a/pom.xml +++ b/pom.xml @@ -1356,7 +1356,7 @@ com.google.code.gson gson - 2.13.1 + 2.13.2 @@ -295,7 +295,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.4.2 + 4.9.6.0 Max Low @@ -305,7 +305,7 @@ com.github.spotbugs spotbugs - 4.9.4 + 4.9.6 @@ -357,13 +357,13 @@ org.sonatype.central central-publishing-maven-plugin - 0.8.0 + 0.9.0 org.apache.maven.plugins maven-javadoc-plugin - 3.11.3 + 3.12.0 false @@ -680,7 +680,7 @@ org.codehaus.mojo license-maven-plugin - 2.6.0 + 2.7.0 false From 5bc41cb8ab5aa381e9f3fbb15c588a8e551332a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:34:10 +0000 Subject: [PATCH 356/701] Bump the test-tools group with 2 updates Bumps the test-tools group with 2 updates: [com.h2database:h2](https://github.com/h2database/h2database) and [org.xmlunit:xmlunit-core](https://github.com/xmlunit/xmlunit). Updates `com.h2database:h2` from 2.3.232 to 2.4.240 - [Release notes](https://github.com/h2database/h2database/releases) - [Commits](https://github.com/h2database/h2database/compare/version-2.3.232...version-2.4.240) Updates `org.xmlunit:xmlunit-core` from 2.10.3 to 2.10.4 - [Release notes](https://github.com/xmlunit/xmlunit/releases) - [Changelog](https://github.com/xmlunit/xmlunit/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/xmlunit/xmlunit/compare/v2.10.3...v2.10.4) --- updated-dependencies: - dependency-name: com.h2database:h2 dependency-version: 2.4.240 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: test-tools - dependency-name: org.xmlunit:xmlunit-core dependency-version: 2.10.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d073d26f46d8..923dc2ba341b 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -905,7 +905,7 @@ org.xmlunit xmlunit-core - 2.10.3 + 2.10.4 test diff --git a/pom.xml b/pom.xml index ae2906714962..ca758cc9942c 100644 --- a/pom.xml +++ b/pom.xml @@ -1684,7 +1684,7 @@ com.h2database h2 - 2.3.232 + 2.4.240 test From fa9df3a4ca1863b64e990ef791d30bb4780a175f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:34:22 +0000 Subject: [PATCH 357/701] Bump org.apache.commons:commons-lang3 in the apache-commons group Bumps the apache-commons group with 1 update: org.apache.commons:commons-lang3. Updates `org.apache.commons:commons-lang3` from 3.18.0 to 3.19.0 --- updated-dependencies: - dependency-name: org.apache.commons:commons-lang3 dependency-version: 3.19.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ae2906714962..02567a351269 100644 --- a/pom.xml +++ b/pom.xml @@ -1503,7 +1503,7 @@ org.apache.commons commons-lang3 - 3.18.0 + 3.19.0 From cf0c599b854a31bc99423bad9d816803b59bb9f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:35:50 +0000 Subject: [PATCH 358/701] Bump org.postgresql:postgresql from 42.7.7 to 42.7.8 Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.7 to 42.7.8. - [Release notes](https://github.com/pgjdbc/pgjdbc/releases) - [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md) - [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.7...REL42.7.8) --- updated-dependencies: - dependency-name: org.postgresql:postgresql dependency-version: 42.7.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ae2906714962..2418352ce0b0 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ 5.7.14 5.6.15.Final 6.2.5.Final - 42.7.7 + 42.7.8 8.11.4 3.11.1 From 0953ef445635343327e1031d82c585d3dc83282f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:35:50 +0000 Subject: [PATCH 359/701] Bump bouncycastle.version from 1.81 to 1.82 Bumps `bouncycastle.version` from 1.81 to 1.82. Updates `org.bouncycastle:bcpkix-jdk18on` from 1.81 to 1.82 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `org.bouncycastle:bcprov-jdk18on` from 1.81 to 1.82 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) Updates `org.bouncycastle:bcutil-jdk18on` from 1.81 to 1.82 - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) --- updated-dependencies: - dependency-name: org.bouncycastle:bcpkix-jdk18on dependency-version: '1.82' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.bouncycastle:bcprov-jdk18on dependency-version: '1.82' dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.bouncycastle:bcutil-jdk18on dependency-version: '1.82' dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ae2906714962..837f5ccba990 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 1.7.36 2.9.4 - 1.81 + 1.82 From dd6b3b4a551d48d8c85f1788fc61348815d9879a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:36:10 +0000 Subject: [PATCH 360/701] Bump log4j.version from 2.25.1 to 2.25.2 Bumps `log4j.version` from 2.25.1 to 2.25.2. Updates `org.apache.logging.log4j:log4j-api` from 2.25.1 to 2.25.2 Updates `org.apache.logging.log4j:log4j-core` from 2.25.1 to 2.25.2 Updates `org.apache.logging.log4j:log4j-1.2-api` from 2.25.1 to 2.25.2 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-version: 2.25.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.2 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-1.2-api dependency-version: 2.25.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ae2906714962..97567f2147fe 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 1.1.1 9.4.58.v20250814 - 2.25.1 + 2.25.2 2.0.34 1.19.0 1.7.36 From 832b641b404247752d6cfee8a1e4737e03a7aadd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 02:36:55 +0000 Subject: [PATCH 361/701] Bump org.scala-lang:scala-library from 2.13.16 to 2.13.17 Bumps [org.scala-lang:scala-library](https://github.com/scala/scala) from 2.13.16 to 2.13.17. - [Release notes](https://github.com/scala/scala/releases) - [Commits](https://github.com/scala/scala/compare/v2.13.16...v2.13.17) --- updated-dependencies: - dependency-name: org.scala-lang:scala-library dependency-version: 2.13.17 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d073d26f46d8..7183395d89e4 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -926,7 +926,7 @@ org.scala-lang scala-library - 2.13.16 + 2.13.17 test From fc74a7ffdf344bc4a1a271cce3dc69dc474e4908 Mon Sep 17 00:00:00 2001 From: nwoodward Date: Wed, 1 Oct 2025 13:41:41 -0500 Subject: [PATCH 362/701] fix Hibernate bugs --- .../content/dao/impl/BitstreamDAOImpl.java | 2 +- .../content/dao/impl/CollectionDAOImpl.java | 25 ++++++++++++++++--- .../org/dspace/core/AbstractHibernateDAO.java | 10 ++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java index 276ea2b3430a..27ef8fc65aec 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java @@ -152,7 +152,7 @@ public int countDeleted(Context context) throws SQLException { @Override public int countWithNoPolicy(Context context) throws SQLException { Query query = createQuery(context, - "SELECT count(bit.id) from Bitstream bit where bit.deleted<>true and bit.id not in" + + "SELECT count(bit.id) from Bitstream bit where bit.deleted<>true and bit not in" + " (select res.dSpaceObject from ResourcePolicy res where res.resourceTypeId = " + ":typeId )"); query.setParameter("typeId", Constants.BITSTREAM); diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java index e5d05abe5d67..0f485a173385 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java @@ -12,6 +12,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.UUID; + import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; @@ -19,6 +21,7 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.ResourcePolicy_; import org.dspace.content.Collection; @@ -40,6 +43,11 @@ * @author kevinvandevelde at atmire.com */ public class CollectionDAOImpl extends AbstractHibernateDSODAO implements CollectionDAO { + /** + * log4j logger + */ + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(CollectionDAOImpl.class); + protected CollectionDAOImpl() { super(); } @@ -172,14 +180,25 @@ public int countRows(Context context) throws SQLException { @SuppressWarnings("unchecked") public List> getCollectionsWithBitstreamSizesTotal(Context context) throws SQLException { - String q = "select col as collection, sum(bit.sizeBytes) as totalBytes from Item i join i.collections col " + - "join i.bundles bun join bun.bitstreams bit group by col"; + String q = "select col.id, sum(bit.sizeBytes) as totalBytes from Item i join i.collections col " + + "join i.bundles bun join bun.bitstreams bit group by col.id"; Query query = createQuery(context, q); + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + List list = query.getResultList(); List> returnList = new ArrayList<>(list.size()); for (Object[] o : list) { - returnList.add(new AbstractMap.SimpleEntry<>((Collection) o[0], (Long) o[1])); + CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(Collection.class); + Root collectionRoot = criteriaQuery.from(Collection.class); + criteriaQuery.select(collectionRoot).where(criteriaBuilder.equal(collectionRoot.get("id"), (UUID) o[0])); + Query collectionQuery = createQuery(context, criteriaQuery); + Collection collection = (Collection) collectionQuery.getSingleResult(); + if (collection != null) { + returnList.add(new AbstractMap.SimpleEntry<>(collection, (Long) o[1])); + } else { + log.warn("Unable to find Collection with UUID: {}", o[0]); + } } return returnList; } diff --git a/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java b/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java index 2101f521854f..b410cd5a7fe3 100644 --- a/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java +++ b/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java @@ -464,4 +464,14 @@ public List findByX(Context context, Class clazz, Map equals, return executeCriteriaQuery(context, criteria, cacheable, maxResults, offset); } + /** + * Create a Query object from a CriteriaQuery + * @param context current Context + * @param criteriaQuery CriteriaQuery built via CriteriaBuilder + * @return corresponding Query + * @throws SQLException if error occurs + */ + public Query createQuery(Context context, CriteriaQuery criteriaQuery) throws SQLException { + return this.getHibernateSession(context).createQuery(criteriaQuery); + } } From 29e13b77fc9f7bff159c76f61e40126a759cf43d Mon Sep 17 00:00:00 2001 From: nwoodward Date: Wed, 1 Oct 2025 13:46:40 -0500 Subject: [PATCH 363/701] checkstyle fix --- .../main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java index 0f485a173385..2dd2bad26a84 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java @@ -13,7 +13,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; - import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; From d8fbe16ede592cffe840a389e58887df1282a4ed Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Wed, 1 Oct 2025 17:24:51 -0300 Subject: [PATCH 364/701] fix(#10721): Sanitize non-characters during OAI indexing (#11139) * fix(#10721): Sanitize non-characters during OAI indexing * refactor: Use StringEscapeUtils as suggested in review * fix: Removed whitespace before the import that was causing error Maven Unit Test failed due to the whitespace before the import https://github.com/DSpace/DSpace/actions/runs/16891881837/job/47853392956?pr=11139#step:4:1959 * fix: Removed trailing whitespace that was causing error --- .../java/org/dspace/xoai/util/ItemUtils.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java index 8f8b7380307a..af130ccb5bdf 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java @@ -16,6 +16,7 @@ import com.lyncode.xoai.dataprovider.xml.xoai.Element; import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; import com.lyncode.xoai.util.Base64Utils; +import org.apache.commons.text.StringEscapeUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.util.factory.UtilServiceFactory; @@ -159,6 +160,19 @@ private static Element createBundlesElement(Context context, Item item) throws S return bundles; } + /** + * Sanitizes a string to remove characters that are invalid + * in XML 1.0 using the Apache Commons Text library. + * @param value The string to sanitize. + * @return A sanitized string, or null if the input was null. + */ + private static String sanitize(String value) { + if (value == null) { + return null; + } + return StringEscapeUtils.escapeXml10(value); + } + private static Element createLicenseElement(Context context, Item item) throws SQLException, AuthorizeException, IOException { Element license = create("license"); @@ -232,7 +246,7 @@ private static void fillSchemaElement(Element schema, MetadataValue val) throws valueElem = language; } - valueElem.getField().add(createValue("value", val.getValue())); + valueElem.getField().add(createValue("value", sanitize(val.getValue()))); if (val.getAuthority() != null) { valueElem.getField().add(createValue("authority", val.getAuthority())); if (val.getConfidence() != Choices.CF_NOVALUE) { From 521e281e4b4b5e8fd56876ee58d8d19fd385a6c8 Mon Sep 17 00:00:00 2001 From: DSpace Bot <68393067+dspace-bot@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:38:54 -0500 Subject: [PATCH 365/701] [Port dspace-8_x] fix(#10721): Sanitize non-characters during OAI indexing (#11397) * fix(#10721): Sanitize non-characters during OAI indexing (cherry picked from commit ad890b0661f696fd2f15cc7fb4aaddd23dbd7bd8) * refactor: Use StringEscapeUtils as suggested in review (cherry picked from commit 0ddd5ad575bdc67ee9bd2d00898f569f38b6c8a8) * fix: Removed whitespace before the import that was causing error Maven Unit Test failed due to the whitespace before the import https://github.com/DSpace/DSpace/actions/runs/16891881837/job/47853392956?pr=11139#step:4:1959 (cherry picked from commit 406bffdccad644f8f6b9ae2fe79cd9fcdfa7959c) * fix: Removed trailing whitespace that was causing error (cherry picked from commit 266ac423b4f3d75369ed790dc632155cbd17909e) --------- Co-authored-by: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> --- .../java/org/dspace/xoai/util/ItemUtils.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java index c3364fb44259..78c98533e0b4 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java @@ -18,6 +18,7 @@ import com.lyncode.xoai.dataprovider.xml.xoai.Element; import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; import com.lyncode.xoai.util.Base64Utils; +import org.apache.commons.text.StringEscapeUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.util.factory.UtilServiceFactory; @@ -165,6 +166,19 @@ private static Element createBundlesElement(Context context, Item item) throws S return bundles; } + /** + * Sanitizes a string to remove characters that are invalid + * in XML 1.0 using the Apache Commons Text library. + * @param value The string to sanitize. + * @return A sanitized string, or null if the input was null. + */ + private static String sanitize(String value) { + if (value == null) { + return null; + } + return StringEscapeUtils.escapeXml10(value); + } + /** * This method will add metadata information about associated resource policies for a give bitstream. * It will parse of relevant policies and add metadata information @@ -281,7 +295,7 @@ private static void fillSchemaElement(Element schema, MetadataValue val) throws valueElem = language; } - valueElem.getField().add(createValue("value", val.getValue())); + valueElem.getField().add(createValue("value", sanitize(val.getValue()))); if (val.getAuthority() != null) { valueElem.getField().add(createValue("authority", val.getAuthority())); if (val.getConfidence() != Choices.CF_NOVALUE) { From e5135a3a8b595d1e81e996d2ea6a1af2f1461c61 Mon Sep 17 00:00:00 2001 From: nwoodward Date: Wed, 17 Sep 2025 11:46:05 -0500 Subject: [PATCH 366/701] added limit to number of typed links to bitstreams to display, falls back to Link Sets if limit is exceeded --- .../service/impl/LinksetServiceImpl.java | 35 ++++++++++- .../controller/LinksetRestControllerIT.java | 58 +++++++++++++++++++ dspace/config/modules/signposting.cfg | 7 ++- 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java index 42b1c8184957..b439a46eda6f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java @@ -18,6 +18,7 @@ import org.dspace.app.rest.security.BitstreamMetadataReadPermissionEvaluatorPlugin; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.processor.bitstream.BitstreamSignpostingProcessor; +import org.dspace.app.rest.signposting.processor.item.ItemLinksetProcessor; import org.dspace.app.rest.signposting.processor.item.ItemSignpostingProcessor; import org.dspace.app.rest.signposting.processor.metadata.MetadataSignpostingProcessor; import org.dspace.app.rest.signposting.service.LinksetService; @@ -28,6 +29,7 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; import org.dspace.utils.DSpace; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -40,12 +42,18 @@ public class LinksetServiceImpl implements LinksetService { private static final Logger log = LogManager.getLogger(LinksetServiceImpl.class); + @Autowired + private ConfigurationService configurationService; + @Autowired protected ItemService itemService; @Autowired private BitstreamMetadataReadPermissionEvaluatorPlugin bitstreamMetadataReadPermissionEvaluatorPlugin; + @Autowired + ItemLinksetProcessor itemLinksetProcessor; + private final List bitstreamProcessors = new DSpace().getServiceManager() .getServicesByType(BitstreamSignpostingProcessor.class); @@ -74,10 +82,20 @@ public List createLinksetNodesForSingleLinkset( Context context, DSpaceObject object ) { + int itemBitstreamsLimit = configurationService.getIntProperty("signposting.item.bitstreams.limit", 10); + List linksetNodes = new ArrayList<>(); if (object.getType() == Constants.ITEM) { - for (ItemSignpostingProcessor processor : itemProcessors) { - processor.addLinkSetNodes(context, request, (Item) object, linksetNodes); + int itemBitstreamsCount = countItemBitstreams((Item) object); + + // Do not include individual bitstream typed links if their number exceeds + // the limit in the configuration. + if (itemBitstreamsCount < itemBitstreamsLimit) { + for (ItemSignpostingProcessor processor : itemProcessors) { + processor.addLinkSetNodes(context, request, (Item) object, linksetNodes); + } + } else { + itemLinksetProcessor.addLinkSetNodes(context, request, (Item) object, linksetNodes); } } else if (object.getType() == Constants.BITSTREAM) { for (BitstreamSignpostingProcessor processor : bitstreamProcessors) { @@ -151,4 +169,17 @@ private Iterator getItemBitstreams(Context context, Item item) { throw new RuntimeException(e); } } + + private int countItemBitstreams(Item item) { + try { + int countBitstreams = 0; + List bundles = itemService.getBundles(item, Constants.DEFAULT_BUNDLE_NAME); + for (Bundle bundle: bundles) { + countBitstreams += bundle.getBitstreams().size(); + } + return countBitstreams; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java index cf62d5ac0861..50b7dbdc6533 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.signposting.controller; import static org.dspace.content.MetadataSchemaEnum.PERSON; +import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -19,7 +20,9 @@ import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.time.Period; +import java.util.ArrayList; import java.util.Date; +import java.util.UUID; import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; @@ -692,6 +695,61 @@ public void findTypedLinkForItemWithAuthor() throws Exception { "&& @.type == 'application/linkset+json')]").exists()); } + @Test + public void showTypedLinksMissingForItemWithMoreBitstreamsThanLimit() throws Exception { + String bitstreamContent = "ThisIsSomeDummyText"; + String bitstreamMimeType = "text/plain"; + + int itemBitstreamsLimit = configurationService.getIntProperty("signposting.item.bitstreams.limit", 10); + + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Item Test") + .withMetadata("dc", "identifier", "doi", doi) + .build(); + + // Add more bitstreams than the configured limit + ArrayList bitstreamIDs = new ArrayList<>(); + for (int i = 0; i <= itemBitstreamsLimit; i++) { + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder.createBitstream(context, item, is) + .withName("Bitstream " + i) + .withDescription("description") + .withMimeType(bitstreamMimeType) + .build(); + + if (bitstream != null) { + bitstreamIDs.add(bitstream.getID()); + } + } + } + context.restoreAuthSystemState(); + + // Make sure the bitstreams were successfully added. + assertTrue("There was a problem ingesting bitstreams.", bitstreamIDs.size() > itemBitstreamsLimit); + + String url = configurationService.getProperty("dspace.ui.url"); + String signpostingUrl = configurationService.getProperty("signposting.path"); + + // There should be typed links to the Link Sets but no typed links to the Bitstreams in the response. + // We only need to check for one of the Bitstream UUIDs, since all of them should be absent. + UUID firstBitstreamId = bitstreamIDs.get(0); + getClient().perform(get("/signposting/links/" + item.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/" + signpostingUrl + "/linksets/" + + item.getID().toString() + "' " + + "&& @.rel == 'linkset' " + + "&& @.type == 'application/linkset')]").exists()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/" + signpostingUrl + "/linksets/" + + item.getID().toString() + "/json' " + + "&& @.rel == 'linkset' " + + "&& @.type == 'application/linkset+json')]").exists()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/bitstreams/" + firstBitstreamId + "/download' " + + "&& @.rel == 'item' " + + "&& @.type == 'text/plain')]").doesNotExist());; + } + @Test public void findTypedLinkForBitstream() throws Exception { String bitstreamContent = "ThisIsSomeDummyText"; diff --git a/dspace/config/modules/signposting.cfg b/dspace/config/modules/signposting.cfg index 6acfa74bdd07..eb2aa81d36b0 100644 --- a/dspace/config/modules/signposting.cfg +++ b/dspace/config/modules/signposting.cfg @@ -34,5 +34,10 @@ signposting.describedby.crosswalk-name = DataCite # Mime-type of response of handling of 'describedby' links. signposting.describedby.mime-type = application/vnd.datacite.datacite+xml +# Limit to the number of an item's bitstreams to return as typed links. +# If there are more bitstreams than this limit then only the typed links to the Link Sets are added to the header. +# Defaults to 10 if the value is unspecified +# signposting.item.bitstreams.limit = 10 + # Optional, to expose the profile attribute, required by PCI workflow () -# signposting.describedby.profile = http://datacite.org/schema/kernel-4 \ No newline at end of file +# signposting.describedby.profile = http://datacite.org/schema/kernel-4 From b5501981ab09efc1a9788d0df7722cae1cf8d4fd Mon Sep 17 00:00:00 2001 From: nwoodward Date: Wed, 17 Sep 2025 11:46:05 -0500 Subject: [PATCH 367/701] added limit to number of typed links to bitstreams to display, falls back to Link Sets if limit is exceeded --- .../service/impl/LinksetServiceImpl.java | 35 ++++++++++- .../controller/LinksetRestControllerIT.java | 58 +++++++++++++++++++ dspace/config/modules/signposting.cfg | 7 ++- 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java index de5556173557..8ce992d1e883 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java @@ -18,6 +18,7 @@ import org.dspace.app.rest.security.BitstreamMetadataReadPermissionEvaluatorPlugin; import org.dspace.app.rest.signposting.model.LinksetNode; import org.dspace.app.rest.signposting.processor.bitstream.BitstreamSignpostingProcessor; +import org.dspace.app.rest.signposting.processor.item.ItemLinksetProcessor; import org.dspace.app.rest.signposting.processor.item.ItemSignpostingProcessor; import org.dspace.app.rest.signposting.processor.metadata.MetadataSignpostingProcessor; import org.dspace.app.rest.signposting.service.LinksetService; @@ -28,6 +29,7 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; import org.dspace.utils.DSpace; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -40,12 +42,18 @@ public class LinksetServiceImpl implements LinksetService { private static final Logger log = LogManager.getLogger(LinksetServiceImpl.class); + @Autowired + private ConfigurationService configurationService; + @Autowired protected ItemService itemService; @Autowired private BitstreamMetadataReadPermissionEvaluatorPlugin bitstreamMetadataReadPermissionEvaluatorPlugin; + @Autowired + ItemLinksetProcessor itemLinksetProcessor; + private final List bitstreamProcessors = new DSpace().getServiceManager() .getServicesByType(BitstreamSignpostingProcessor.class); @@ -74,10 +82,20 @@ public List createLinksetNodesForSingleLinkset( Context context, DSpaceObject object ) { + int itemBitstreamsLimit = configurationService.getIntProperty("signposting.item.bitstreams.limit", 10); + List linksetNodes = new ArrayList<>(); if (object.getType() == Constants.ITEM) { - for (ItemSignpostingProcessor processor : itemProcessors) { - processor.addLinkSetNodes(context, request, (Item) object, linksetNodes); + int itemBitstreamsCount = countItemBitstreams((Item) object); + + // Do not include individual bitstream typed links if their number exceeds + // the limit in the configuration. + if (itemBitstreamsCount < itemBitstreamsLimit) { + for (ItemSignpostingProcessor processor : itemProcessors) { + processor.addLinkSetNodes(context, request, (Item) object, linksetNodes); + } + } else { + itemLinksetProcessor.addLinkSetNodes(context, request, (Item) object, linksetNodes); } } else if (object.getType() == Constants.BITSTREAM) { for (BitstreamSignpostingProcessor processor : bitstreamProcessors) { @@ -151,4 +169,17 @@ private Iterator getItemBitstreams(Context context, Item item) { throw new RuntimeException(e); } } + + private int countItemBitstreams(Item item) { + try { + int countBitstreams = 0; + List bundles = itemService.getBundles(item, Constants.DEFAULT_BUNDLE_NAME); + for (Bundle bundle: bundles) { + countBitstreams += bundle.getBitstreams().size(); + } + return countBitstreams; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java index a65357f97bfe..167fbeb9e3f3 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/signposting/controller/LinksetRestControllerIT.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.signposting.controller; import static org.dspace.content.MetadataSchemaEnum.PERSON; +import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; @@ -18,7 +19,9 @@ import java.text.DateFormat; import java.text.MessageFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.UUID; import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; @@ -691,6 +694,61 @@ public void findTypedLinkForItemWithAuthor() throws Exception { "&& @.type == 'application/linkset+json')]").exists()); } + @Test + public void showTypedLinksMissingForItemWithMoreBitstreamsThanLimit() throws Exception { + String bitstreamContent = "ThisIsSomeDummyText"; + String bitstreamMimeType = "text/plain"; + + int itemBitstreamsLimit = configurationService.getIntProperty("signposting.item.bitstreams.limit", 10); + + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Item Test") + .withMetadata("dc", "identifier", "doi", doi) + .build(); + + // Add more bitstreams than the configured limit + ArrayList bitstreamIDs = new ArrayList<>(); + for (int i = 0; i <= itemBitstreamsLimit; i++) { + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder.createBitstream(context, item, is) + .withName("Bitstream " + i) + .withDescription("description") + .withMimeType(bitstreamMimeType) + .build(); + + if (bitstream != null) { + bitstreamIDs.add(bitstream.getID()); + } + } + } + context.restoreAuthSystemState(); + + // Make sure the bitstreams were successfully added. + assertTrue("There was a problem ingesting bitstreams.", bitstreamIDs.size() > itemBitstreamsLimit); + + String url = configurationService.getProperty("dspace.ui.url"); + String signpostingUrl = configurationService.getProperty("signposting.path"); + + // There should be typed links to the Link Sets but no typed links to the Bitstreams in the response. + // We only need to check for one of the Bitstream UUIDs, since all of them should be absent. + UUID firstBitstreamId = bitstreamIDs.get(0); + getClient().perform(get("/signposting/links/" + item.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/" + signpostingUrl + "/linksets/" + + item.getID().toString() + "' " + + "&& @.rel == 'linkset' " + + "&& @.type == 'application/linkset')]").exists()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/" + signpostingUrl + "/linksets/" + + item.getID().toString() + "/json' " + + "&& @.rel == 'linkset' " + + "&& @.type == 'application/linkset+json')]").exists()) + .andExpect(jsonPath("$[?(@.href == '" + url + "/bitstreams/" + firstBitstreamId + "/download' " + + "&& @.rel == 'item' " + + "&& @.type == 'text/plain')]").doesNotExist());; + } + @Test public void findTypedLinkForBitstream() throws Exception { String bitstreamContent = "ThisIsSomeDummyText"; diff --git a/dspace/config/modules/signposting.cfg b/dspace/config/modules/signposting.cfg index fba80da41481..5f8d933a92b2 100644 --- a/dspace/config/modules/signposting.cfg +++ b/dspace/config/modules/signposting.cfg @@ -32,4 +32,9 @@ signposting.enabled = true signposting.describedby.crosswalk-name = DataCite # Mime-type of response of handling of 'describedby' links. -signposting.describedby.mime-type = application/vnd.datacite.datacite+xml \ No newline at end of file +signposting.describedby.mime-type = application/vnd.datacite.datacite+xml + +# Limit to the number of an item's bitstreams to return as typed links. +# If there are more bitstreams than this limit then only the typed links to the Link Sets are added to the header. +# Defaults to 10 if the value is unspecified +# signposting.item.bitstreams.limit = 10 \ No newline at end of file From 7cbe8dfae393fa8a1bbd2ca16435e961bf3e17ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:53:16 +0000 Subject: [PATCH 368/701] Bump the build-tools group with 2 updates Bumps the build-tools group with 2 updates: [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) and [org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin). Updates `org.apache.maven.plugins:maven-enforcer-plugin` from 3.6.1 to 3.6.2 - [Release notes](https://github.com/apache/maven-enforcer/releases) - [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.6.1...enforcer-3.6.2) Updates `org.apache.maven.plugins:maven-dependency-plugin` from 3.8.1 to 3.9.0 - [Release notes](https://github.com/apache/maven-dependency-plugin/releases) - [Commits](https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.8.1...maven-dependency-plugin-3.9.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-enforcer-plugin dependency-version: 3.6.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-dependency-plugin dependency-version: 3.9.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 5a49541c2a12..0ad4f62f26d2 100644 --- a/pom.xml +++ b/pom.xml @@ -89,7 +89,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.6.1 + 3.6.2 enforce-java @@ -348,7 +348,7 @@ org.apache.maven.plugins maven-dependency-plugin - 3.8.1 + 3.9.0 org.apache.maven.plugins From 4037b05e5c7e5e40556d212b486aefc9ad65ef8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:55:13 +0000 Subject: [PATCH 369/701] Bump pdfbox-version from 2.0.34 to 2.0.35 Bumps `pdfbox-version` from 2.0.34 to 2.0.35. Updates `org.apache.pdfbox:pdfbox` from 2.0.34 to 2.0.35 Updates `org.apache.pdfbox:fontbox` from 2.0.34 to 2.0.35 --- updated-dependencies: - dependency-name: org.apache.pdfbox:pdfbox dependency-version: 2.0.35 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.pdfbox:fontbox dependency-version: 2.0.35 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5a49541c2a12..08ad9c717b35 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ 9.4.58.v20250814 2.25.2 - 2.0.34 + 2.0.35 1.19.0 2.0.17 2.9.4 From 245003658907c405e4f9f509dd1d14207c2feb84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:55:41 +0000 Subject: [PATCH 370/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.791 to 1.12.792 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.791 to 1.12.792. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.791...1.12.792) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-version: 1.12.792 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 1aa596458823..2a14560d4f26 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -717,7 +717,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.791 + 1.12.792 6.4.10.Final 8.0.3.Final - 42.7.7 + 42.7.8 10.22.0 8.11.4 From 7aa29edafd4cbbb578823813ea5fc5cdde08fc47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:59:17 +0000 Subject: [PATCH 374/701] Bump pdfbox-version from 2.0.34 to 2.0.35 Bumps `pdfbox-version` from 2.0.34 to 2.0.35. Updates `org.apache.pdfbox:pdfbox` from 2.0.34 to 2.0.35 Updates `org.apache.pdfbox:fontbox` from 2.0.34 to 2.0.35 --- updated-dependencies: - dependency-name: org.apache.pdfbox:pdfbox dependency-version: 2.0.35 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.pdfbox:fontbox dependency-version: 2.0.35 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f5671523c55b..015d23c85dbd 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 9.4.58.v20250814 2.25.2 - 2.0.34 + 2.0.35 1.19.0 1.7.36 2.9.4 From 2a4ac311c2fe93f35f84165a08ba1fc242b5dbaa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:00:09 +0000 Subject: [PATCH 375/701] Bump com.amazonaws:aws-java-sdk-s3 from 1.12.791 to 1.12.792 Bumps [com.amazonaws:aws-java-sdk-s3](https://github.com/aws/aws-sdk-java) from 1.12.791 to 1.12.792. - [Changelog](https://github.com/aws/aws-sdk-java/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-java/compare/1.12.791...1.12.792) --- updated-dependencies: - dependency-name: com.amazonaws:aws-java-sdk-s3 dependency-version: 1.12.792 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 923dc2ba341b..2e9d6df1dfeb 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -738,7 +738,7 @@ com.amazonaws aws-java-sdk-s3 - 1.12.791 + 1.12.792 From e6a818fe7b085e07fd6462ce564ddba65c774e57 Mon Sep 17 00:00:00 2001 From: Andreas Czerniak Date: Thu, 9 Oct 2025 16:41:28 +0200 Subject: [PATCH 376/701] =?UTF-8?q?Terminology=20update:=20=E2=80=98OpenAI?= =?UTF-8?q?RE=20Guidelines=20for=20Literature=20Repository=20Managers=20v4?= =?UTF-8?q?=E2=80=99=20=E2=86=92=20=E2=80=98OpenAIRE=20Guidelines=20for=20?= =?UTF-8?q?Institutional=20and=20Thematic=20Repository=20Managers=20v4?= =?UTF-8?q?=E2=80=99.=20Descriptions=20and=20links=20have=20been=20aligned?= =?UTF-8?q?=20with=20the=20current=20standard.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oai/metadataFormats/oai_openaire.xsl | 70 +++++++++---------- .../crosswalks/oai/transformers/openaire4.xsl | 6 +- dspace/config/crosswalks/oai/xoai.xml | 6 +- .../entities/openaire4-relationships.xml | 12 ++-- dspace/config/registries/openaire4-types.xml | 10 +-- dspace/config/submission-forms.xml | 2 +- 6 files changed, 53 insertions(+), 53 deletions(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 905187494dd3..2b388ceac7c0 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -101,7 +101,7 @@ - + @@ -137,7 +137,7 @@ - + @@ -206,7 +206,7 @@ - + @@ -406,7 +406,7 @@ - + @@ -482,7 +482,7 @@ - + @@ -592,7 +592,7 @@ - + @@ -611,7 +611,7 @@ - + @@ -633,7 +633,7 @@ - + @@ -663,7 +663,7 @@ - + @@ -697,7 +697,7 @@ - + @@ -717,8 +717,8 @@ - - + + @@ -729,7 +729,7 @@ - + @@ -739,7 +739,7 @@ @@ -772,7 +772,7 @@ - + @@ -784,7 +784,7 @@ - + @@ -795,7 +795,7 @@ - + @@ -820,7 +820,7 @@ - + @@ -833,7 +833,7 @@ - + @@ -849,7 +849,7 @@ - + @@ -868,7 +868,7 @@ - + @@ -913,7 +913,7 @@ - + @@ -922,7 +922,7 @@ - + @@ -931,7 +931,7 @@ - + @@ -940,7 +940,7 @@ - + @@ -949,7 +949,7 @@ - + @@ -958,7 +958,7 @@ - + @@ -967,7 +967,7 @@ - + @@ -977,7 +977,7 @@ - + @@ -1039,7 +1039,7 @@ - + @@ -1051,7 +1051,7 @@ - + Available @@ -1393,7 +1393,7 @@ @@ -1446,7 +1446,7 @@ This template will return the COAR Resource Type Vocabulary URI like http://purl.org/coar/resource_type/c_6501 based on a valued text like 'article' - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_publicationtype.html#attribute-uri-m + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_publicationtype.html#attribute-uri-m --> @@ -1642,7 +1642,7 @@ like "open access" based on the values from DSpace Access Status mechanism like String 'open.access' please check class org.dspace.access.status.DefaultAccessStatusHelper for more information - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> @@ -1672,7 +1672,7 @@ This template will return the COAR Access Right Vocabulary URI like http://purl.org/coar/access_right/c_abf2 based on a value text like 'open access' - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> diff --git a/dspace/config/crosswalks/oai/transformers/openaire4.xsl b/dspace/config/crosswalks/oai/transformers/openaire4.xsl index cece890450b7..e9be2d54edb3 100644 --- a/dspace/config/crosswalks/oai/transformers/openaire4.xsl +++ b/dspace/config/crosswalks/oai/transformers/openaire4.xsl @@ -13,7 +13,7 @@ @@ -80,7 +80,7 @@ Normalizing dc.rights according to COAR Controlled Vocabulary for Access Rights (Version 1.0) (http://vocabularies.coar-repositories.org/documentation/access_rights/) available at - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> @@ -116,7 +116,7 @@ diff --git a/dspace/config/crosswalks/oai/xoai.xml b/dspace/config/crosswalks/oai/xoai.xml index e843814dd7b8..8ecaeca84ee7 100644 --- a/dspace/config/crosswalks/oai/xoai.xml +++ b/dspace/config/crosswalks/oai/xoai.xml @@ -73,7 +73,7 @@ - This contexts complies with OpenAIRE Guidelines for Literature Repositories v4.0. + This contexts complies with OpenAIRE Guidelines for Institutional and Thematic Repository Managers v4.0. @@ -177,7 +177,7 @@ http://irdb.nii.ac.jp/oai/junii2-3-1.xsd oai_openaire diff --git a/dspace/config/entities/openaire4-relationships.xml b/dspace/config/entities/openaire4-relationships.xml index daa0e2c1da9c..d77371b603a4 100644 --- a/dspace/config/entities/openaire4-relationships.xml +++ b/dspace/config/entities/openaire4-relationships.xml @@ -3,7 +3,7 @@ - Publication @@ -17,7 +17,7 @@ 0 - Publication @@ -31,7 +31,7 @@ 0 - Publication @@ -45,7 +45,7 @@ 0 - Publication @@ -59,7 +59,7 @@ 0 - Publication @@ -73,7 +73,7 @@ 0 - Project diff --git a/dspace/config/registries/openaire4-types.xml b/dspace/config/registries/openaire4-types.xml index b3290ac12038..749b13fa3152 100644 --- a/dspace/config/registries/openaire4-types.xml +++ b/dspace/config/registries/openaire4-types.xml @@ -2,13 +2,13 @@ - OpenAIRE4 fields definition + OpenAIRE v4 fields definition @@ -108,7 +108,7 @@ datacite @@ -116,7 +116,7 @@ Spatial region or named place where the data was gathered or about which the data is focused. - + datacite subject diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index bdee08e75347..ff4af24fb3e7 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -1557,7 +1557,7 @@ From f41c929cddfa99b1c93b5b362259ac0360957b47 Mon Sep 17 00:00:00 2001 From: Andreas Czerniak Date: Thu, 9 Oct 2025 16:41:28 +0200 Subject: [PATCH 377/701] =?UTF-8?q?Terminology=20update:=20=E2=80=98OpenAI?= =?UTF-8?q?RE=20Guidelines=20for=20Literature=20Repository=20Managers=20v4?= =?UTF-8?q?=E2=80=99=20=E2=86=92=20=E2=80=98OpenAIRE=20Guidelines=20for=20?= =?UTF-8?q?Institutional=20and=20Thematic=20Repository=20Managers=20v4?= =?UTF-8?q?=E2=80=99.=20Descriptions=20and=20links=20have=20been=20aligned?= =?UTF-8?q?=20with=20the=20current=20standard.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oai/metadataFormats/oai_openaire.xsl | 70 +++++++++---------- .../crosswalks/oai/transformers/openaire4.xsl | 8 +-- dspace/config/crosswalks/oai/xoai.xml | 8 +-- .../entities/openaire4-relationships.xml | 12 ++-- dspace/config/registries/openaire4-types.xml | 8 +-- dspace/config/submission-forms.xml | 4 +- 6 files changed, 55 insertions(+), 55 deletions(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index fdf93da5ae31..8042a8cf6fd9 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -101,7 +101,7 @@ - + @@ -137,7 +137,7 @@ - + @@ -206,7 +206,7 @@ - + @@ -406,7 +406,7 @@ - + @@ -489,7 +489,7 @@ - + @@ -599,7 +599,7 @@ - + @@ -618,7 +618,7 @@ - + @@ -640,7 +640,7 @@ - + @@ -670,7 +670,7 @@ - + @@ -704,7 +704,7 @@ - + @@ -724,8 +724,8 @@ - - + + @@ -736,7 +736,7 @@ - + @@ -746,7 +746,7 @@ @@ -779,7 +779,7 @@ - + @@ -791,7 +791,7 @@ - + @@ -802,7 +802,7 @@ - + @@ -827,7 +827,7 @@ - + @@ -840,7 +840,7 @@ - + @@ -856,7 +856,7 @@ - + @@ -875,7 +875,7 @@ - + @@ -945,7 +945,7 @@ - + @@ -954,7 +954,7 @@ - + @@ -963,7 +963,7 @@ - + @@ -972,7 +972,7 @@ - + @@ -981,7 +981,7 @@ - + @@ -990,7 +990,7 @@ - + @@ -999,7 +999,7 @@ - + @@ -1009,7 +1009,7 @@ - + @@ -1071,7 +1071,7 @@ - + @@ -1083,7 +1083,7 @@ - + Available @@ -1425,7 +1425,7 @@ @@ -1478,7 +1478,7 @@ This template will return the COAR Resource Type Vocabulary URI like http://purl.org/coar/resource_type/c_6501 based on a valued text like 'article' - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_publicationtype.html#attribute-uri-m + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_publicationtype.html#attribute-uri-m --> @@ -1674,7 +1674,7 @@ like "open access" based on the values from DSpace Access Status mechanism like String 'open.access' please check class org.dspace.access.status.DefaultAccessStatusHelper for more information - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> @@ -1704,7 +1704,7 @@ This template will return the COAR Access Right Vocabulary URI like http://purl.org/coar/access_right/c_abf2 based on a value text like 'open access' - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> diff --git a/dspace/config/crosswalks/oai/transformers/openaire4.xsl b/dspace/config/crosswalks/oai/transformers/openaire4.xsl index 8ac703609d6f..7f26b81c344a 100644 --- a/dspace/config/crosswalks/oai/transformers/openaire4.xsl +++ b/dspace/config/crosswalks/oai/transformers/openaire4.xsl @@ -12,8 +12,8 @@ @@ -80,7 +80,7 @@ Normalizing dc.rights according to COAR Controlled Vocabulary for Access Rights (Version 1.0) (http://vocabularies.coar-repositories.org/documentation/access_rights/) available at - https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/v4.0.0/field_accessrights.html#definition-and-usage-instruction + https://openaire-guidelines-for-literature-repository-managers.readthedocs.io/en/4.0.1/field_accessrights.html#definition-and-usage-instruction --> @@ -116,7 +116,7 @@ diff --git a/dspace/config/crosswalks/oai/xoai.xml b/dspace/config/crosswalks/oai/xoai.xml index 0f1cdf7d68cd..da51eb5eaf77 100644 --- a/dspace/config/crosswalks/oai/xoai.xml +++ b/dspace/config/crosswalks/oai/xoai.xml @@ -74,7 +74,7 @@ - This contexts complies with Openaire Guidelines for Literature Repositories v4.0. + This contexts complies with OpenAIRE Guidelines for Institutional and Thematic Repository Managers v4.0. oai_openaire diff --git a/dspace/config/entities/openaire4-relationships.xml b/dspace/config/entities/openaire4-relationships.xml index daa0e2c1da9c..d77371b603a4 100644 --- a/dspace/config/entities/openaire4-relationships.xml +++ b/dspace/config/entities/openaire4-relationships.xml @@ -3,7 +3,7 @@ - Publication @@ -17,7 +17,7 @@ 0 - Publication @@ -31,7 +31,7 @@ 0 - Publication @@ -45,7 +45,7 @@ 0 - Publication @@ -59,7 +59,7 @@ 0 - Publication @@ -73,7 +73,7 @@ 0 - Project diff --git a/dspace/config/registries/openaire4-types.xml b/dspace/config/registries/openaire4-types.xml index e47e06e0aebf..17dccb222dc8 100644 --- a/dspace/config/registries/openaire4-types.xml +++ b/dspace/config/registries/openaire4-types.xml @@ -2,13 +2,13 @@ - Openaire4 fields definition + OpenAIRE v4 fields definition @@ -102,4 +102,4 @@ The date when the conference took place. This property is considered to be part of the bibliographic citation. Recommended best practice for encoding the date value is defined in a profile of ISO 8601 [W3CDTF] and follows the YYYY-MM-DD format. - + \ No newline at end of file diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index c3e47a71b811..0c5d00e4aa62 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -1556,8 +1556,8 @@ - From c17d0a6e2a389ad5468e508191e8cbef1c1f823f Mon Sep 17 00:00:00 2001 From: Joran De Braekeleer Date: Mon, 8 Sep 2025 12:42:39 +0200 Subject: [PATCH 378/701] 133566: Add group-eperson logging (cherry picked from commit b6fefd46aabeb173694fe2e050e4b39abb131961) --- .../main/java/org/dspace/eperson/GroupServiceImpl.java | 8 ++++++++ dspace/config/dstat.map | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java index 4cec4c9c0d93..44727d3e5fb9 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java @@ -142,6 +142,8 @@ public void addMember(Context context, Group group, EPerson e) { context.addEvent( new Event(Event.ADD, Constants.GROUP, group.getID(), Constants.EPERSON, e.getID(), e.getEmail(), getIdentifiers(context, group))); + log.info(LogHelper.getHeader(context, "add_group_eperson", + "group_id=" + group.getID() + ", eperson_id=" + e.getID())); } @Override @@ -157,6 +159,8 @@ public void addMember(Context context, Group groupParent, Group groupChild) thro context.addEvent(new Event(Event.ADD, Constants.GROUP, groupParent.getID(), Constants.GROUP, groupChild.getID(), groupChild.getName(), getIdentifiers(context, groupParent))); + log.info(LogHelper.getHeader(context, "add_group_subgroup", + "group_id=" + groupParent.getID() + ", subgroup_id=" + groupChild.getID())); } /** @@ -214,6 +218,8 @@ public void removeMember(Context context, Group group, EPerson ePerson) throws S if (group.remove(ePerson)) { context.addEvent(new Event(Event.REMOVE, Constants.GROUP, group.getID(), Constants.EPERSON, ePerson.getID(), ePerson.getEmail(), getIdentifiers(context, group))); + log.info(LogHelper.getHeader(context, "remove_group_eperson", + "group_id=" + group.getID() + ", eperson_id=" + ePerson.getID())); } } @@ -242,6 +248,8 @@ public void removeMember(Context context, Group groupParent, Group childGroup) t context.addEvent( new Event(Event.REMOVE, Constants.GROUP, groupParent.getID(), Constants.GROUP, childGroup.getID(), childGroup.getName(), getIdentifiers(context, groupParent))); + log.info(LogHelper.getHeader(context, "remove_group_subgroup", + "group_id=" + groupParent.getID() + ", subgroup_id=" + childGroup.getID())); } } diff --git a/dspace/config/dstat.map b/dspace/config/dstat.map index 140049ee13a1..bfd08ede3c56 100644 --- a/dspace/config/dstat.map +++ b/dspace/config/dstat.map @@ -100,4 +100,8 @@ show_feedback_form=Feedback Form Displayed create_dc_type=New Dublin Core Type Created remove_template_item=Item Template Removed withdraw_item=Item Withdrawn -download_export_archive = Download Export Archive \ No newline at end of file +download_export_archive = Download Export Archive +add_group_eperson = EPerson Added to Group +remove_group_eperson = EPerson Removed from Group +add_group_subgroup = Child Group Added to Group +remove_group_subgroup = Child Group Removed from Group \ No newline at end of file From 7bfe4265c8bf27c600d604272de7961bbf461751 Mon Sep 17 00:00:00 2001 From: Joran De Braekeleer Date: Mon, 8 Sep 2025 12:42:39 +0200 Subject: [PATCH 379/701] 133566: Add group-eperson logging (cherry picked from commit b6fefd46aabeb173694fe2e050e4b39abb131961) --- .../main/java/org/dspace/eperson/GroupServiceImpl.java | 8 ++++++++ dspace/config/dstat.map | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java index 4cec4c9c0d93..44727d3e5fb9 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java @@ -142,6 +142,8 @@ public void addMember(Context context, Group group, EPerson e) { context.addEvent( new Event(Event.ADD, Constants.GROUP, group.getID(), Constants.EPERSON, e.getID(), e.getEmail(), getIdentifiers(context, group))); + log.info(LogHelper.getHeader(context, "add_group_eperson", + "group_id=" + group.getID() + ", eperson_id=" + e.getID())); } @Override @@ -157,6 +159,8 @@ public void addMember(Context context, Group groupParent, Group groupChild) thro context.addEvent(new Event(Event.ADD, Constants.GROUP, groupParent.getID(), Constants.GROUP, groupChild.getID(), groupChild.getName(), getIdentifiers(context, groupParent))); + log.info(LogHelper.getHeader(context, "add_group_subgroup", + "group_id=" + groupParent.getID() + ", subgroup_id=" + groupChild.getID())); } /** @@ -214,6 +218,8 @@ public void removeMember(Context context, Group group, EPerson ePerson) throws S if (group.remove(ePerson)) { context.addEvent(new Event(Event.REMOVE, Constants.GROUP, group.getID(), Constants.EPERSON, ePerson.getID(), ePerson.getEmail(), getIdentifiers(context, group))); + log.info(LogHelper.getHeader(context, "remove_group_eperson", + "group_id=" + group.getID() + ", eperson_id=" + ePerson.getID())); } } @@ -242,6 +248,8 @@ public void removeMember(Context context, Group groupParent, Group childGroup) t context.addEvent( new Event(Event.REMOVE, Constants.GROUP, groupParent.getID(), Constants.GROUP, childGroup.getID(), childGroup.getName(), getIdentifiers(context, groupParent))); + log.info(LogHelper.getHeader(context, "remove_group_subgroup", + "group_id=" + groupParent.getID() + ", subgroup_id=" + childGroup.getID())); } } diff --git a/dspace/config/dstat.map b/dspace/config/dstat.map index 140049ee13a1..bfd08ede3c56 100644 --- a/dspace/config/dstat.map +++ b/dspace/config/dstat.map @@ -100,4 +100,8 @@ show_feedback_form=Feedback Form Displayed create_dc_type=New Dublin Core Type Created remove_template_item=Item Template Removed withdraw_item=Item Withdrawn -download_export_archive = Download Export Archive \ No newline at end of file +download_export_archive = Download Export Archive +add_group_eperson = EPerson Added to Group +remove_group_eperson = EPerson Removed from Group +add_group_subgroup = Child Group Added to Group +remove_group_subgroup = Child Group Removed from Group \ No newline at end of file From d512533f6f1c5e85fc1a16daeb4c9ad786fd0359 Mon Sep 17 00:00:00 2001 From: fribeiro-fccn <65163266+fribeiro-fccn@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:29:08 +0100 Subject: [PATCH 380/701] Fix 10740: Add more efficient Collection.findAuthorized() to improve performance of SWORDv2 servicedocument (#11333) * Changed Authentication for a user in Sword - Added new methods to handle the queries to get collections of the current user only * Changed to handle the community request if get communities is enabled in config * Added javadoc * Correct stylecheck * Corrected comments and changed AND to OR as the action_id should be an OR * Removed trailing space * Changed visibilty to getParentGroups to public and changed the DAO method to use Hibernate and bypass the WITH RECURSIVE sql query and the need the use o NativeQuery * Added Constants.ADMIN * Removed unused code * Refactor of DAO method to use CritriaBuilder * Changed return method * Changed findByEPersonGroupTypeIdAction it returns a List and maxim results was set to 1. Changed to -1 to get the multiple results * Changed the Service to get all collections when a user is admin of a Community * Minor Update to sword authenticator * Added some more tests --- .../dao/impl/ResourcePolicyDAOImpl.java | 2 +- .../dspace/content/CollectionServiceImpl.java | 84 +++++++++ .../org/dspace/content/dao/CollectionDAO.java | 12 ++ .../content/dao/impl/CollectionDAOImpl.java | 100 +++++++++- .../content/service/CollectionService.java | 12 ++ .../main/java/org/dspace/eperson/Group.java | 2 +- .../org/dspace/content/CollectionTest.java | 178 ++++++++++++++++++ .../org/dspace/sword2/SwordAuthenticator.java | 41 ++-- 8 files changed, 409 insertions(+), 22 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java b/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java index 3b09f9cf300b..1f9e5ea26677 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java @@ -182,7 +182,7 @@ public List findByEPersonGroupTypeIdAction(Context context, EPer compareEpersonOrGroups ) ); - return list(context, criteriaQuery, false, ResourcePolicy.class, 1, -1); + return list(context, criteriaQuery, false, ResourcePolicy.class, -1, -1); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index f5ef4f4b14a4..10f458b81975 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -13,13 +13,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; +import java.util.Queue; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -838,6 +842,86 @@ public List findAuthorized(Context context, Community community, int return myResults; } + @Override + public List findAuthorized(Context context, Community community, List actions) + throws SQLException { + + List myCollections = new ArrayList<>(); + EPerson eperson = context.getCurrentUser(); + + //If eperson is Administrator return all colls or if a community is not null only the community's collections + if (authorizeService.isAdmin(context, eperson)) { + if (community != null) { + return community.getCollections(); + } + myCollections = this.findAll(context); + return myCollections; + } + + //Get the collections of the eperson where is is admin of a community + List directGroups = new ArrayList<>(eperson.getGroups()); // direct membership + Queue queue = new LinkedList<>(directGroups); + while (!queue.isEmpty()) { + Group current = queue.poll(); + List parents = current.getParentGroups(); + + for (Group parent : parents) { + if (directGroups.add(parent)) { + queue.add(parent); + } + } + } + + List resourcePolicies = resourcePolicyService + .find(context, eperson, directGroups, Constants.ADMIN, Constants.COMMUNITY); + List uuids = resourcePolicies.stream() + .map(policy -> policy.getdSpaceObject().getID()) + .toList(); + + List communities = uuids.stream() + .map(uuid -> { + try { + return communityService.find(context, uuid); + } catch (SQLException e) { + return null; //ignore that uuid + } + }) + .filter(Objects::nonNull) + .toList(); + + Set allCommunities = new HashSet<>(communities); + Set allCommAdminCollections = communities.stream() + .flatMap(cm -> cm.getCollections().stream()) + .collect(Collectors.toSet()); + Queue queueComm = new LinkedList<>(communities); + + while (!queueComm.isEmpty()) { + Community com = queueComm.poll(); + List childrenComms = com.getSubcommunities(); + for (Community childComm : childrenComms) { + if (allCommunities.add(childComm)) { + queueComm.add(childComm); + allCommAdminCollections.addAll(childComm.getCollections()); + } + } + } + + //Now get the collection when the eperson can deposit or is admin or is in a group with those privileges + myCollections = collectionDAO.findAuthorizedByEPerson(context, eperson, actions); + Set allCollections = new HashSet<>(myCollections); + //Join EPerson Community Admin Collections with Collection Admins + allCollections.addAll(allCommAdminCollections); + + List collsAllowed = new ArrayList<>(allCollections); + + //A community is passed, only the community's collections will be used and existing in eperson Authorizations + if (community != null) { + collsAllowed.retainAll(community.getCollections()); + } + + return collsAllowed; + } + @Override public Collection findByGroup(Context context, Group group) throws SQLException { return collectionDAO.findByGroup(context, group); diff --git a/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java index 6bb65bbb46d8..13bcf5f52c02 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java @@ -48,6 +48,18 @@ public List findAll(Context context, MetadataField order, Integer li List findAuthorizedByGroup(Context context, EPerson ePerson, List actions) throws SQLException; + /** + * Get all authorized collections of the current EPerson + * + * @param context DSpace context object + * @param ePerson the current EPerson + * @param actions list of actionsID ADD, READ, etc. + * @return the collections the eperson is defined + * @throws SQLException if database error + */ + List findAuthorizedByEPerson(Context context, EPerson ePerson, List actions) + throws SQLException; + List findCollectionsWithSubscribers(Context context) throws SQLException; int countRows(Context context) throws SQLException; diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java index 4530e6eeda40..e47b1ed4a02b 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java @@ -10,8 +10,12 @@ import java.sql.SQLException; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; +import java.util.Set; import java.util.UUID; import jakarta.persistence.Query; @@ -164,6 +168,100 @@ public List findAuthorizedByGroup(Context context, EPerson ePerson, } + /** + * Get all authorized collections of the current EPerson + * + * @param context DSpace context object + * @param ePerson the current EPerson + * @param actions list of actionsID ADD, READ, etc. + * @return the collections the eperson is defined + * @throws SQLException if database error + */ + @Override + public List findAuthorizedByEPerson(Context context, EPerson ePerson, List actions) + throws SQLException { + + //NOTE steps 1) and 2) removes the need of WITH RECURSIVE and a NativeQuery + + // 1) Get all groups a eperson belongs + /*ArrayList<>(ePerson.getGroups()) - This ensures you have a concrete copy and can modify it safely. + instead if List directGroups = ePerson.getGroups(); + Also - Can be done using this query: + List directGroups = createQuery(context, """ + SELECT g + FROM Group g + JOIN g.epeople e + WHERE e.id = :epersonId + """) + .setParameter("epersonId", ePerson.getID()) + .getResultList(); + */ + List directGroups = new ArrayList<>(ePerson.getGroups()); // direct membership + + // 2) Expand hierarquy of groups in memory (recursively) + Set allGroups = new HashSet<>(directGroups); + Queue queue = new LinkedList<>(directGroups); + + /* + * Using the query avoids the change of the getParentGroups visibility in Group + * The List parents = current.getParentGroups() could be achieved using: + * List parents = createQuery(context,""" + SELECT g + FROM Group g + JOIN g.groups child + WHERE child = :child + """) + */ + // //current.getMemberGroups()- Making public getParentGroups in Group Class (why it isn't already public?) + while (!queue.isEmpty()) { + Group current = queue.poll(); + List parents = current.getParentGroups(); + + for (Group parent : parents) { + if (allGroups.add(parent)) { + queue.add(parent); + } + } + } + + CriteriaBuilder cb = getCriteriaBuilder(context); + CriteriaQuery cq = getCriteriaQuery(cb, Collection.class); + Root collectionRoot = cq.from(Collection.class); + + // Join to ResourcePolicy using metamodel + Join rpJoin = collectionRoot.join("resourcePolicies"); + // Use metamodel for typesafe access + cq.select(collectionRoot).distinct(true); + + List predicates = new ArrayList<>(actions.size()); + // WHERE rp.resourceTypeId = :resourceType + predicates.add(cb.equal(rpJoin.get(ResourcePolicy_.resourceTypeId), Constants.COLLECTION)); + // AND (:hasActions = false OR rp.actionId IN :actionIds) + if (actions != null && !actions.isEmpty()) { + predicates.add(rpJoin.get(ResourcePolicy_.actionId).in(actions)); + } + + // AND (rp.eperson.id = :epersonId OR (:hasGroups = true AND rp.epersonGroup.id IN :groupIds)) + Predicate epersonPredicate = cb.equal( + rpJoin.get(ResourcePolicy_.eperson), ePerson + ); + // Using only groups instead of groupsIDs + Predicate groupPredicate = cb.disjunction(); // false by default + if (allGroups != null && !allGroups.isEmpty()) { + groupPredicate = rpJoin.get(ResourcePolicy_.epersonGroup).in(allGroups); + } + + // Combine access condition + Predicate accessPredicate = cb.or(epersonPredicate, groupPredicate); + predicates.add(accessPredicate); + + // Apply WHERE clause + cq.where(cb.and(predicates.toArray(new Predicate[0]))); + + // Execute + return list(context, cq, true, Collection.class, -1, -1); + } + @Override public List findCollectionsWithSubscribers(Context context) throws SQLException { return list(createQuery(context, "SELECT DISTINCT c FROM Collection c JOIN Subscription s ON c = " + @@ -201,4 +299,4 @@ public List> getCollectionsWithBitstreamSizesTotal(C } return returnList; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java index 3a865d9d63fd..c2b633821376 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java @@ -327,6 +327,18 @@ public void canEdit(Context context, Collection collection, boolean useInheritan public List findAuthorized(Context context, Community community, int actionID) throws java.sql.SQLException; + /** + * return an array of collections that user has a given permission on + * + * @param context DSpace Context + * @param community (optional) restrict search to a community, else null + * @param actions Listo of the of the action ADD, READ, ADMIN, etc. + * @return Collection [] of collections with matching permissions + * @throws SQLException if database error + */ + public List findAuthorized(Context context, Community community, List actions) + throws java.sql.SQLException; + /** * * @param context DSpace Context diff --git a/dspace-api/src/main/java/org/dspace/eperson/Group.java b/dspace-api/src/main/java/org/dspace/eperson/Group.java index 24b44b8149a4..8146bf702d0a 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/Group.java +++ b/dspace-api/src/main/java/org/dspace/eperson/Group.java @@ -138,7 +138,7 @@ boolean contains(EPerson e) { return getMembers().contains(e); } - List getParentGroups() { + public List getParentGroups() { return parentGroups; } diff --git a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java index a177571ffa46..d943a4a31613 100644 --- a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java +++ b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java @@ -1159,6 +1159,184 @@ public void testFindAuthorizedOptimized() throws Exception { assertFalse("testFindAuthorizeOptimized D.C", personDCollections.contains(collectionC)); } + /** + * Test of findAuthorizedEpersonAndGroups method, of class Collection. + * We create some collections and a user and groups and subgroups and add the user to one subgroup + * and one collection + * The parent group will be added to the other collection. + */ + @Test + public void testFindAuthorizedByEPerson() throws Exception { + context.turnOffAuthorisationSystem(); + Community com = communityService.create(null, context); + Collection collectionA = collectionService.create(context, com); + Collection collectionB = collectionService.create(context, com); + Collection collectionC = collectionService.create(context, com); + + com.addCollection(collectionA); + com.addCollection(collectionB); + com.addCollection(collectionC); + + Group groupParent = groupService.create(context); + Group groupChild = groupService.create(context); + + groupService.addMember(context, groupParent, groupChild); + + EPerson epersonA = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, groupChild, epersonA); + + //personA can submit to collectionA and collectionC + authorizeService.addPolicy(context, collectionA, Constants.ADD, epersonA); + authorizeService.addPolicy(context, collectionB, Constants.ADD, groupParent); + + context.restoreAuthSystemState(); + + context.setCurrentUser(epersonA); + List personACollections = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedByEPerson A", personACollections.size() == 2); + assertTrue("testFindAuthorizedByEPerson A.A", personACollections.contains(collectionA)); + assertTrue("testFindAuthorizedByEPerson A.B", personACollections.contains(collectionB)); + assertFalse("testFindAuthorizedByEPerson A.C", personACollections.contains(collectionC)); + } + + /** + * Test of testFindAuthorizedEPersonCommunityAdmin method, of class Collection. + * This will test what collections care retrieved if a user is a Com Administrator + * eperson A is Top of B (and by the caso of B,C and D) but not of E + * eperson E is Top of E nad of D so it can get THE E and D Collections + * + */ + @Test + public void testFindAuthorizedEPersonCommunityAdmin() throws Exception { + context.turnOffAuthorisationSystem(); + Community comA = communityService.create(null, context); + Community comB = communityService.create(null, context); + Community comC = communityService.create(null, context); + Community comD = communityService.create(null, context); + Community comE = communityService.create(null, context); + + Collection collectionA1 = collectionService.create(context, comA); + Collection collectionC1 = collectionService.create(context, comC); + Collection collectionC2 = collectionService.create(context, comC); + Collection collectionD1 = collectionService.create(context, comD); + Collection collectionE1 = collectionService.create(context, comE); + Collection collectionE2 = collectionService.create(context, comE); + + //Create Com hierarchies + comA.addSubCommunity(comB); + comA.addSubCommunity(comC); + comB.addSubCommunity(comD); + + comA.addCollection(collectionA1); + comC.addCollection(collectionC1); + comC.addCollection(collectionC2); + comD.addCollection(collectionD1); + comE.addCollection(collectionE1); + comE.addCollection(collectionE2); + + + Group groupA = groupService.create(context); + Group groupB = groupService.create(context); + Group groupC = groupService.create(context); + Group groupD = groupService.create(context); + Group groupE = groupService.create(context); + + EPerson epersonA = ePersonService.create(context); + EPerson epersonB = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, groupA, epersonA); + //Add epersonB to the child group + groupService.addMember(context, groupE, epersonB); + groupService.addMember(context, groupD, epersonB); + + //personA can submit to collectionA and collectionB + authorizeService.addPolicy(context, comA, Constants.ADMIN, groupA); + authorizeService.addPolicy(context, comD, Constants.ADMIN, groupD); + authorizeService.addPolicy(context, comE, Constants.ADMIN, groupE); + + context.restoreAuthSystemState(); + + //PersonA Can get AllCollection From Top to Bottom com ComA, but not from ComE + context.setCurrentUser(epersonA); + List personACollectionsAdminCommA = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonCommunityAdmin A", personACollectionsAdminCommA.size() == 4); + assertTrue("testFindAuthorizedEPersonCommunityAdmin A.A", personACollectionsAdminCommA + .containsAll(List.of(collectionA1, collectionD1, collectionC1, collectionC2))); + assertFalse("testFindAuthorizedEPersonCommunityAdmin A.B", personACollectionsAdminCommA + .containsAll(List.of(collectionE1, collectionE2))); + + //PersonB Can get AllCollection From Top to Bottom com ComE, but not from ComA + context.setCurrentUser(epersonB); + List personACollectionsAdminCommE = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonCommunityAdmin B", personACollectionsAdminCommE.size() == 3); + assertFalse("testFindAuthorizedEPersonCommunityAdmin B.A", personACollectionsAdminCommE + .containsAll(List.of(collectionA1, collectionC1, collectionC2))); + assertTrue("testFindAuthorizedEPersonCommunityAdmin B.B", personACollectionsAdminCommE + .containsAll(List.of(collectionD1, collectionE1, collectionE2))); + } + + /** + * Test of testFindNotAuthorizedEPersonDifferentActions method, of class Collection. + * We create some collections and a user and a group add the user as ADMIN by adding ti + * toa group and that group to a Collection and add the user as submitter to another + * we pass actions that shouldn't return collections if only those actions are passed + * And we test if only on collection is retrieved if we pass the Corresponding action + */ + @Test + public void testFindAuthorizedEPersonDifferentActions() throws Exception { + context.turnOffAuthorisationSystem(); + Community com = communityService.create(null, context); + Collection collectionA = collectionService.create(context, com); + Collection collectionB = collectionService.create(context, com); + + com.addCollection(collectionA); + com.addCollection(collectionB); + + Group group = groupService.create(context); + + EPerson epersonA = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, group, epersonA); + + //personA can submit to collectionA and collectionB + authorizeService.addPolicy(context, collectionA, Constants.ADD, epersonA); + authorizeService.addPolicy(context, collectionB, Constants.ADMIN, group); + + context.restoreAuthSystemState(); + + //Person does not Have other permission than ADD - So should not return a Colelction if we pass other + //Actions. In this case WRITE OR DELETE + context.setCurrentUser(epersonA); + List personACollectionsRD = + collectionService.findAuthorized(context, null, List.of(Constants.WRITE, Constants.DELETE)); + assertTrue("testFindAuthorizedEPersonDifferentActions A", personACollectionsRD.isEmpty()); + assertFalse("testFindAuthorizedEPersonDifferentActions A.A", personACollectionsRD.contains(collectionA)); + assertFalse("testFindAuthorizedEPersonDifferentActions A.B", personACollectionsRD.contains(collectionB)); + + //But It Should get Collection B if we pass the ADMIN Action too + List personACollectionsADD = + collectionService.findAuthorized(context, null, + List.of(Constants.WRITE, Constants.DELETE, Constants.ADD)); + assertTrue("testFindAuthorizedEPersonDifferentActions B", personACollectionsADD.size() == 1); + assertTrue("testFindAuthorizedEPersonDifferentActions B.A", personACollectionsADD.contains(collectionA)); + assertFalse("testFindAuthorizedEPersonDifferentActions B.B", personACollectionsADD.contains(collectionB)); + + //But It Should get Collection A if we pass the ADD Action too + List personACollections = + collectionService.findAuthorized(context, null, + List.of(Constants.WRITE, Constants.DELETE, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonDifferentActions C", personACollections.size() == 1); + assertFalse("testFindAuthorizedEPersonDifferentActions C.A", personACollections.contains(collectionA)); + assertTrue("testFindAuthorizedEPersonDifferentActions C.B", personACollections.contains(collectionB)); + } + /** * Test of countItems method, of class Collection. */ diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java index 54b769388c68..e47c0f076b98 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -618,31 +619,33 @@ public List getAllowedCollections( // short cut by obtaining the collections to which the authenticated user can submit List cols = collectionService.findAuthorized( - authContext, community, Constants.ADD); + authContext, community, Arrays.asList(Constants.ADD, Constants.ADMIN)); + List allowed = new ArrayList<>(); // now find out if the obo user is allowed to submit to any of these collections - for (Collection col : cols) { - boolean oboAllowed = false; - - // check for obo null - if (swordContext.getOnBehalfOf() == null) { - oboAllowed = true; - } - - // if we have not already determined that the obo user is ok to submit, look up the READ policy on the - // community. THis will include determining if the user is an administrator. - if (!oboAllowed) { - oboAllowed = authorizeService.authorizeActionBoolean( - swordContext.getOnBehalfOfContext(), col, - Constants.ADD); - } + if (swordContext.getOnBehalfOf() != null) { + for (Collection col : cols) { + boolean oboAllowed = false; + + //if we have not already determined that the obo user is ok to submit, + //look up the READ policy on the + // community. THis will include determining if the user is an administrator. + if (!oboAllowed) { + oboAllowed = authorizeService.authorizeActionBoolean( + swordContext.getOnBehalfOfContext(), col, + Constants.ADD); + } - // final check to see if we are allowed to READ - if (oboAllowed) { - allowed.add(col); + // final check to see if we are allowed to READ + if (oboAllowed) { + allowed.add(col); + } } + } else { + return cols; } + return allowed; } catch (SQLException e) { From 50e4795b9d63c7a63bfea25e430e86de41604f48 Mon Sep 17 00:00:00 2001 From: fribeiro-fccn <65163266+fribeiro-fccn@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:29:08 +0100 Subject: [PATCH 381/701] Fix 10740: Add more efficient Collection.findAuthorized() to improve performance of SWORDv2 servicedocument (#11333) * Changed Authentication for a user in Sword - Added new methods to handle the queries to get collections of the current user only * Changed to handle the community request if get communities is enabled in config * Added javadoc * Correct stylecheck * Corrected comments and changed AND to OR as the action_id should be an OR * Removed trailing space * Changed visibilty to getParentGroups to public and changed the DAO method to use Hibernate and bypass the WITH RECURSIVE sql query and the need the use o NativeQuery * Added Constants.ADMIN * Removed unused code * Refactor of DAO method to use CritriaBuilder * Changed return method * Changed findByEPersonGroupTypeIdAction it returns a List and maxim results was set to 1. Changed to -1 to get the multiple results * Changed the Service to get all collections when a user is admin of a Community * Minor Update to sword authenticator * Added some more tests --- .../dao/impl/ResourcePolicyDAOImpl.java | 2 +- .../dspace/content/CollectionServiceImpl.java | 84 +++++++++ .../org/dspace/content/dao/CollectionDAO.java | 12 ++ .../content/dao/impl/CollectionDAOImpl.java | 100 +++++++++- .../content/service/CollectionService.java | 12 ++ .../main/java/org/dspace/eperson/Group.java | 2 +- .../org/dspace/content/CollectionTest.java | 178 ++++++++++++++++++ .../org/dspace/sword2/SwordAuthenticator.java | 41 ++-- 8 files changed, 409 insertions(+), 22 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java b/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java index 26b6bb1d7345..96dd08e1ae99 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/dao/impl/ResourcePolicyDAOImpl.java @@ -165,7 +165,7 @@ public List findByEPersonGroupTypeIdAction(Context context, EPer (resourcePolicyRoot.get(ResourcePolicy_.epersonGroup).in(groups))) ) ); - return list(context, criteriaQuery, false, ResourcePolicy.class, 1, -1); + return list(context, criteriaQuery, false, ResourcePolicy.class, -1, -1); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index 1b051ae32ffb..60cf769ae24c 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -13,13 +13,17 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; +import java.util.Queue; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -838,6 +842,86 @@ public List findAuthorized(Context context, Community community, int return myResults; } + @Override + public List findAuthorized(Context context, Community community, List actions) + throws SQLException { + + List myCollections = new ArrayList<>(); + EPerson eperson = context.getCurrentUser(); + + //If eperson is Administrator return all colls or if a community is not null only the community's collections + if (authorizeService.isAdmin(context, eperson)) { + if (community != null) { + return community.getCollections(); + } + myCollections = this.findAll(context); + return myCollections; + } + + //Get the collections of the eperson where is is admin of a community + List directGroups = new ArrayList<>(eperson.getGroups()); // direct membership + Queue queue = new LinkedList<>(directGroups); + while (!queue.isEmpty()) { + Group current = queue.poll(); + List parents = current.getParentGroups(); + + for (Group parent : parents) { + if (directGroups.add(parent)) { + queue.add(parent); + } + } + } + + List resourcePolicies = resourcePolicyService + .find(context, eperson, directGroups, Constants.ADMIN, Constants.COMMUNITY); + List uuids = resourcePolicies.stream() + .map(policy -> policy.getdSpaceObject().getID()) + .toList(); + + List communities = uuids.stream() + .map(uuid -> { + try { + return communityService.find(context, uuid); + } catch (SQLException e) { + return null; //ignore that uuid + } + }) + .filter(Objects::nonNull) + .toList(); + + Set allCommunities = new HashSet<>(communities); + Set allCommAdminCollections = communities.stream() + .flatMap(cm -> cm.getCollections().stream()) + .collect(Collectors.toSet()); + Queue queueComm = new LinkedList<>(communities); + + while (!queueComm.isEmpty()) { + Community com = queueComm.poll(); + List childrenComms = com.getSubcommunities(); + for (Community childComm : childrenComms) { + if (allCommunities.add(childComm)) { + queueComm.add(childComm); + allCommAdminCollections.addAll(childComm.getCollections()); + } + } + } + + //Now get the collection when the eperson can deposit or is admin or is in a group with those privileges + myCollections = collectionDAO.findAuthorizedByEPerson(context, eperson, actions); + Set allCollections = new HashSet<>(myCollections); + //Join EPerson Community Admin Collections with Collection Admins + allCollections.addAll(allCommAdminCollections); + + List collsAllowed = new ArrayList<>(allCollections); + + //A community is passed, only the community's collections will be used and existing in eperson Authorizations + if (community != null) { + collsAllowed.retainAll(community.getCollections()); + } + + return collsAllowed; + } + @Override public Collection findByGroup(Context context, Group group) throws SQLException { return collectionDAO.findByGroup(context, group); diff --git a/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java index 6bb65bbb46d8..13bcf5f52c02 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/CollectionDAO.java @@ -48,6 +48,18 @@ public List findAll(Context context, MetadataField order, Integer li List findAuthorizedByGroup(Context context, EPerson ePerson, List actions) throws SQLException; + /** + * Get all authorized collections of the current EPerson + * + * @param context DSpace context object + * @param ePerson the current EPerson + * @param actions list of actionsID ADD, READ, etc. + * @return the collections the eperson is defined + * @throws SQLException if database error + */ + List findAuthorizedByEPerson(Context context, EPerson ePerson, List actions) + throws SQLException; + List findCollectionsWithSubscribers(Context context) throws SQLException; int countRows(Context context) throws SQLException; diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java index 2dd2bad26a84..e54542eda2dc 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/CollectionDAOImpl.java @@ -10,8 +10,12 @@ import java.sql.SQLException; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; +import java.util.Set; import java.util.UUID; import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; @@ -164,6 +168,100 @@ public List findAuthorizedByGroup(Context context, EPerson ePerson, } + /** + * Get all authorized collections of the current EPerson + * + * @param context DSpace context object + * @param ePerson the current EPerson + * @param actions list of actionsID ADD, READ, etc. + * @return the collections the eperson is defined + * @throws SQLException if database error + */ + @Override + public List findAuthorizedByEPerson(Context context, EPerson ePerson, List actions) + throws SQLException { + + //NOTE steps 1) and 2) removes the need of WITH RECURSIVE and a NativeQuery + + // 1) Get all groups a eperson belongs + /*ArrayList<>(ePerson.getGroups()) - This ensures you have a concrete copy and can modify it safely. + instead if List directGroups = ePerson.getGroups(); + Also - Can be done using this query: + List directGroups = createQuery(context, """ + SELECT g + FROM Group g + JOIN g.epeople e + WHERE e.id = :epersonId + """) + .setParameter("epersonId", ePerson.getID()) + .getResultList(); + */ + List directGroups = new ArrayList<>(ePerson.getGroups()); // direct membership + + // 2) Expand hierarquy of groups in memory (recursively) + Set allGroups = new HashSet<>(directGroups); + Queue queue = new LinkedList<>(directGroups); + + /* + * Using the query avoids the change of the getParentGroups visibility in Group + * The List parents = current.getParentGroups() could be achieved using: + * List parents = createQuery(context,""" + SELECT g + FROM Group g + JOIN g.groups child + WHERE child = :child + """) + */ + // //current.getMemberGroups()- Making public getParentGroups in Group Class (why it isn't already public?) + while (!queue.isEmpty()) { + Group current = queue.poll(); + List parents = current.getParentGroups(); + + for (Group parent : parents) { + if (allGroups.add(parent)) { + queue.add(parent); + } + } + } + + CriteriaBuilder cb = getCriteriaBuilder(context); + CriteriaQuery cq = getCriteriaQuery(cb, Collection.class); + Root collectionRoot = cq.from(Collection.class); + + // Join to ResourcePolicy using metamodel + Join rpJoin = collectionRoot.join("resourcePolicies"); + // Use metamodel for typesafe access + cq.select(collectionRoot).distinct(true); + + List predicates = new ArrayList<>(actions.size()); + // WHERE rp.resourceTypeId = :resourceType + predicates.add(cb.equal(rpJoin.get(ResourcePolicy_.resourceTypeId), Constants.COLLECTION)); + // AND (:hasActions = false OR rp.actionId IN :actionIds) + if (actions != null && !actions.isEmpty()) { + predicates.add(rpJoin.get(ResourcePolicy_.actionId).in(actions)); + } + + // AND (rp.eperson.id = :epersonId OR (:hasGroups = true AND rp.epersonGroup.id IN :groupIds)) + Predicate epersonPredicate = cb.equal( + rpJoin.get(ResourcePolicy_.eperson), ePerson + ); + // Using only groups instead of groupsIDs + Predicate groupPredicate = cb.disjunction(); // false by default + if (allGroups != null && !allGroups.isEmpty()) { + groupPredicate = rpJoin.get(ResourcePolicy_.epersonGroup).in(allGroups); + } + + // Combine access condition + Predicate accessPredicate = cb.or(epersonPredicate, groupPredicate); + predicates.add(accessPredicate); + + // Apply WHERE clause + cq.where(cb.and(predicates.toArray(new Predicate[0]))); + + // Execute + return list(context, cq, true, Collection.class, -1, -1); + } + @Override public List findCollectionsWithSubscribers(Context context) throws SQLException { return list(createQuery(context, "SELECT DISTINCT c FROM Collection c JOIN Subscription s ON c = " + @@ -201,4 +299,4 @@ public List> getCollectionsWithBitstreamSizesTotal(C } return returnList; } -} \ No newline at end of file +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java index fd681f68f013..5e55c725691d 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java @@ -327,6 +327,18 @@ public void canEdit(Context context, Collection collection, boolean useInheritan public List findAuthorized(Context context, Community community, int actionID) throws java.sql.SQLException; + /** + * return an array of collections that user has a given permission on + * + * @param context DSpace Context + * @param community (optional) restrict search to a community, else null + * @param actions Listo of the of the action ADD, READ, ADMIN, etc. + * @return Collection [] of collections with matching permissions + * @throws SQLException if database error + */ + public List findAuthorized(Context context, Community community, List actions) + throws java.sql.SQLException; + /** * * @param context DSpace Context diff --git a/dspace-api/src/main/java/org/dspace/eperson/Group.java b/dspace-api/src/main/java/org/dspace/eperson/Group.java index 67655e0e0aaf..af7d14661d2e 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/Group.java +++ b/dspace-api/src/main/java/org/dspace/eperson/Group.java @@ -142,7 +142,7 @@ boolean contains(EPerson e) { return getMembers().contains(e); } - List getParentGroups() { + public List getParentGroups() { return parentGroups; } diff --git a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java index 13d037abf823..f26043281061 100644 --- a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java +++ b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java @@ -1159,6 +1159,184 @@ public void testFindAuthorizedOptimized() throws Exception { assertFalse("testFindAuthorizeOptimized D.C", personDCollections.contains(collectionC)); } + /** + * Test of findAuthorizedEpersonAndGroups method, of class Collection. + * We create some collections and a user and groups and subgroups and add the user to one subgroup + * and one collection + * The parent group will be added to the other collection. + */ + @Test + public void testFindAuthorizedByEPerson() throws Exception { + context.turnOffAuthorisationSystem(); + Community com = communityService.create(null, context); + Collection collectionA = collectionService.create(context, com); + Collection collectionB = collectionService.create(context, com); + Collection collectionC = collectionService.create(context, com); + + com.addCollection(collectionA); + com.addCollection(collectionB); + com.addCollection(collectionC); + + Group groupParent = groupService.create(context); + Group groupChild = groupService.create(context); + + groupService.addMember(context, groupParent, groupChild); + + EPerson epersonA = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, groupChild, epersonA); + + //personA can submit to collectionA and collectionC + authorizeService.addPolicy(context, collectionA, Constants.ADD, epersonA); + authorizeService.addPolicy(context, collectionB, Constants.ADD, groupParent); + + context.restoreAuthSystemState(); + + context.setCurrentUser(epersonA); + List personACollections = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedByEPerson A", personACollections.size() == 2); + assertTrue("testFindAuthorizedByEPerson A.A", personACollections.contains(collectionA)); + assertTrue("testFindAuthorizedByEPerson A.B", personACollections.contains(collectionB)); + assertFalse("testFindAuthorizedByEPerson A.C", personACollections.contains(collectionC)); + } + + /** + * Test of testFindAuthorizedEPersonCommunityAdmin method, of class Collection. + * This will test what collections care retrieved if a user is a Com Administrator + * eperson A is Top of B (and by the caso of B,C and D) but not of E + * eperson E is Top of E nad of D so it can get THE E and D Collections + * + */ + @Test + public void testFindAuthorizedEPersonCommunityAdmin() throws Exception { + context.turnOffAuthorisationSystem(); + Community comA = communityService.create(null, context); + Community comB = communityService.create(null, context); + Community comC = communityService.create(null, context); + Community comD = communityService.create(null, context); + Community comE = communityService.create(null, context); + + Collection collectionA1 = collectionService.create(context, comA); + Collection collectionC1 = collectionService.create(context, comC); + Collection collectionC2 = collectionService.create(context, comC); + Collection collectionD1 = collectionService.create(context, comD); + Collection collectionE1 = collectionService.create(context, comE); + Collection collectionE2 = collectionService.create(context, comE); + + //Create Com hierarchies + comA.addSubCommunity(comB); + comA.addSubCommunity(comC); + comB.addSubCommunity(comD); + + comA.addCollection(collectionA1); + comC.addCollection(collectionC1); + comC.addCollection(collectionC2); + comD.addCollection(collectionD1); + comE.addCollection(collectionE1); + comE.addCollection(collectionE2); + + + Group groupA = groupService.create(context); + Group groupB = groupService.create(context); + Group groupC = groupService.create(context); + Group groupD = groupService.create(context); + Group groupE = groupService.create(context); + + EPerson epersonA = ePersonService.create(context); + EPerson epersonB = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, groupA, epersonA); + //Add epersonB to the child group + groupService.addMember(context, groupE, epersonB); + groupService.addMember(context, groupD, epersonB); + + //personA can submit to collectionA and collectionB + authorizeService.addPolicy(context, comA, Constants.ADMIN, groupA); + authorizeService.addPolicy(context, comD, Constants.ADMIN, groupD); + authorizeService.addPolicy(context, comE, Constants.ADMIN, groupE); + + context.restoreAuthSystemState(); + + //PersonA Can get AllCollection From Top to Bottom com ComA, but not from ComE + context.setCurrentUser(epersonA); + List personACollectionsAdminCommA = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonCommunityAdmin A", personACollectionsAdminCommA.size() == 4); + assertTrue("testFindAuthorizedEPersonCommunityAdmin A.A", personACollectionsAdminCommA + .containsAll(List.of(collectionA1, collectionD1, collectionC1, collectionC2))); + assertFalse("testFindAuthorizedEPersonCommunityAdmin A.B", personACollectionsAdminCommA + .containsAll(List.of(collectionE1, collectionE2))); + + //PersonB Can get AllCollection From Top to Bottom com ComE, but not from ComA + context.setCurrentUser(epersonB); + List personACollectionsAdminCommE = + collectionService.findAuthorized(context, null, List.of(Constants.ADD, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonCommunityAdmin B", personACollectionsAdminCommE.size() == 3); + assertFalse("testFindAuthorizedEPersonCommunityAdmin B.A", personACollectionsAdminCommE + .containsAll(List.of(collectionA1, collectionC1, collectionC2))); + assertTrue("testFindAuthorizedEPersonCommunityAdmin B.B", personACollectionsAdminCommE + .containsAll(List.of(collectionD1, collectionE1, collectionE2))); + } + + /** + * Test of testFindNotAuthorizedEPersonDifferentActions method, of class Collection. + * We create some collections and a user and a group add the user as ADMIN by adding ti + * toa group and that group to a Collection and add the user as submitter to another + * we pass actions that shouldn't return collections if only those actions are passed + * And we test if only on collection is retrieved if we pass the Corresponding action + */ + @Test + public void testFindAuthorizedEPersonDifferentActions() throws Exception { + context.turnOffAuthorisationSystem(); + Community com = communityService.create(null, context); + Collection collectionA = collectionService.create(context, com); + Collection collectionB = collectionService.create(context, com); + + com.addCollection(collectionA); + com.addCollection(collectionB); + + Group group = groupService.create(context); + + EPerson epersonA = ePersonService.create(context); + + //Add epersonA to the child group + groupService.addMember(context, group, epersonA); + + //personA can submit to collectionA and collectionB + authorizeService.addPolicy(context, collectionA, Constants.ADD, epersonA); + authorizeService.addPolicy(context, collectionB, Constants.ADMIN, group); + + context.restoreAuthSystemState(); + + //Person does not Have other permission than ADD - So should not return a Colelction if we pass other + //Actions. In this case WRITE OR DELETE + context.setCurrentUser(epersonA); + List personACollectionsRD = + collectionService.findAuthorized(context, null, List.of(Constants.WRITE, Constants.DELETE)); + assertTrue("testFindAuthorizedEPersonDifferentActions A", personACollectionsRD.isEmpty()); + assertFalse("testFindAuthorizedEPersonDifferentActions A.A", personACollectionsRD.contains(collectionA)); + assertFalse("testFindAuthorizedEPersonDifferentActions A.B", personACollectionsRD.contains(collectionB)); + + //But It Should get Collection B if we pass the ADMIN Action too + List personACollectionsADD = + collectionService.findAuthorized(context, null, + List.of(Constants.WRITE, Constants.DELETE, Constants.ADD)); + assertTrue("testFindAuthorizedEPersonDifferentActions B", personACollectionsADD.size() == 1); + assertTrue("testFindAuthorizedEPersonDifferentActions B.A", personACollectionsADD.contains(collectionA)); + assertFalse("testFindAuthorizedEPersonDifferentActions B.B", personACollectionsADD.contains(collectionB)); + + //But It Should get Collection A if we pass the ADD Action too + List personACollections = + collectionService.findAuthorized(context, null, + List.of(Constants.WRITE, Constants.DELETE, Constants.ADMIN)); + assertTrue("testFindAuthorizedEPersonDifferentActions C", personACollections.size() == 1); + assertFalse("testFindAuthorizedEPersonDifferentActions C.A", personACollections.contains(collectionA)); + assertTrue("testFindAuthorizedEPersonDifferentActions C.B", personACollections.contains(collectionB)); + } + /** * Test of countItems method, of class Collection. */ diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java index 54b769388c68..e47c0f076b98 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordAuthenticator.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -618,31 +619,33 @@ public List getAllowedCollections( // short cut by obtaining the collections to which the authenticated user can submit List cols = collectionService.findAuthorized( - authContext, community, Constants.ADD); + authContext, community, Arrays.asList(Constants.ADD, Constants.ADMIN)); + List allowed = new ArrayList<>(); // now find out if the obo user is allowed to submit to any of these collections - for (Collection col : cols) { - boolean oboAllowed = false; - - // check for obo null - if (swordContext.getOnBehalfOf() == null) { - oboAllowed = true; - } - - // if we have not already determined that the obo user is ok to submit, look up the READ policy on the - // community. THis will include determining if the user is an administrator. - if (!oboAllowed) { - oboAllowed = authorizeService.authorizeActionBoolean( - swordContext.getOnBehalfOfContext(), col, - Constants.ADD); - } + if (swordContext.getOnBehalfOf() != null) { + for (Collection col : cols) { + boolean oboAllowed = false; + + //if we have not already determined that the obo user is ok to submit, + //look up the READ policy on the + // community. THis will include determining if the user is an administrator. + if (!oboAllowed) { + oboAllowed = authorizeService.authorizeActionBoolean( + swordContext.getOnBehalfOfContext(), col, + Constants.ADD); + } - // final check to see if we are allowed to READ - if (oboAllowed) { - allowed.add(col); + // final check to see if we are allowed to READ + if (oboAllowed) { + allowed.add(col); + } } + } else { + return cols; } + return allowed; } catch (SQLException e) { From 00ca9ccded6cf3dc4fc66dfa7d43a898de6ef712 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 21 Oct 2025 16:11:44 -0500 Subject: [PATCH 382/701] Stream.toList() doesn't exist in Java 11. Replace with Stream.collect(Collectors.toList()) --- .../main/java/org/dspace/content/CollectionServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index 60cf769ae24c..beef87582ea2 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -876,7 +876,7 @@ public List findAuthorized(Context context, Community community, Lis .find(context, eperson, directGroups, Constants.ADMIN, Constants.COMMUNITY); List uuids = resourcePolicies.stream() .map(policy -> policy.getdSpaceObject().getID()) - .toList(); + .collect(Collectors.toList()); List communities = uuids.stream() .map(uuid -> { @@ -887,7 +887,7 @@ public List findAuthorized(Context context, Community community, Lis } }) .filter(Objects::nonNull) - .toList(); + .collect(Collectors.toList()); Set allCommunities = new HashSet<>(communities); Set allCommAdminCollections = communities.stream() From dc338fcb501762f928eafe35cb8f5ad2f391f905 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Thu, 18 Sep 2025 15:23:06 +0200 Subject: [PATCH 383/701] 134319: Escape HTML tags from certain fields before applying hit highlights Co-authored-by: bram.maegerman@atmire.com --- .../org/dspace/discovery/SolrServiceImpl.java | 13 ++++ .../app/rest/DiscoveryRestControllerIT.java | 77 +++++++++++++++++++ dspace/config/modules/discovery.cfg | 8 +- dspace/solr/search/conf/solrconfig.xml | 6 ++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index 0cf2aa50af67..3dafaf241bd1 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -18,6 +18,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.Date; @@ -956,8 +957,20 @@ protected SolrQuery resolveToSolrQuery(Context context, DiscoverQuery discoveryQ if (0 < discoveryQuery.getHitHighlightingFields().size()) { solrQuery.setHighlight(true); solrQuery.add(HighlightParams.USE_PHRASE_HIGHLIGHTER, Boolean.TRUE.toString()); + boolean escapeHTML = configurationService.getBooleanProperty("discovery.highlights.escape-html", true); + String[] renderHTMLForFields = + configurationService.getArrayProperty("discovery.highlights.html-allowed-fields"); for (DiscoverHitHighlightingField highlightingField : discoveryQuery.getHitHighlightingFields()) { solrQuery.addHighlightField(highlightingField.getField() + "_hl"); + boolean allowHTMLInField = Arrays.stream(renderHTMLForFields) + .anyMatch(field -> highlightingField.getField().matches(field)); + if (!escapeHTML || allowHTMLInField) { + solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.METHOD, "original"); + } else { + solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.METHOD, "unified"); + solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.ENCODER, "html"); + } + solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.FRAGSIZE, String.valueOf(highlightingField.getMaxChars())); solrQuery.add("f." + highlightingField.getField() + "_hl." + HighlightParams.SNIPPETS, diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java index a115c8aa2f15..bb29601be8ce 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java @@ -85,6 +85,28 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest @Autowired ChoiceAuthorityService choiceAuthorityService; + /** + * Original value of the discovery.highlights.escape-html property, saved here to restore it after running the + * tests. + */ + boolean escapeHTML; + + @Override + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + escapeHTML = configurationService.getBooleanProperty("discovery.highlights.escape-html"); + context.restoreAuthSystemState(); + } + + @Override + public void destroy() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("discovery.highlights.escape-html", escapeHTML); + context.restoreAuthSystemState(); + super.destroy(); + } + @Test public void rootDiscoverTest() throws Exception { @@ -6805,4 +6827,59 @@ public void discoverSearchObjectsSupervisionConfigurationTest() throws Exception .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects"))); } + @Test + public void discoverSearchObjectsFirstEscapeHTMLTagsBeforeApplyingHitHighlights() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("discovery.highlights.escape-html", true); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + + ItemBuilder.createItem(context, col1) + .withTitle("This is a test title") + .build(); + context.restoreAuthSystemState(); + + // This test proves that the HTML tags that are in the original metadata, like test, + // are now escaped and should be returned like <a>test</a> + // Only after this happens should the hit highlights be applied + getClient().perform(get("/api/discover/search/objects") + .param("query", "title")) + .andExpect(status().isOk()) + .andExpect(jsonPath( + "$._embedded.searchResult._embedded.objects[0].hitHighlights['dc.title']", + contains("This is a <a>test</a> title"))); + } + + @Test + public void discoverSearchObjectsDontEscapeHTMLTagsBeforeApplyingHitHighlights() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("discovery.highlights.escape-html", false); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + + ItemBuilder.createItem(context, col1) + .withTitle("This is a test title") + .build(); + context.restoreAuthSystemState(); + + // This test proves that the HTML tags that are in the original metadata, like test, + // are not escaped and should be returned like test + // Only after this happens should the hit highlights be applied + getClient().perform(get("/api/discover/search/objects") + .param("query", "title")) + .andExpect(status().isOk()) + .andExpect(jsonPath( + "$._embedded.searchResult._embedded.objects[0].hitHighlights['dc.title']", + contains("This is a test title"))); + } } diff --git a/dspace/config/modules/discovery.cfg b/dspace/config/modules/discovery.cfg index 72088ddc49fa..dcf2cd9e942b 100644 --- a/dspace/config/modules/discovery.cfg +++ b/dspace/config/modules/discovery.cfg @@ -48,4 +48,10 @@ discovery.facet.namedtype.workflow.pooled = 004workflow\n|||\nWaiting for Contro # Set the number of retry of a query when stale objects are found. # Set to -1 if stale objects should be ignored. Set to 0 if you want to avoid extra query but take the chance to cleanup # the index each time that stale objects are found. Default 3 -discovery.removestale.attempts = 3 \ No newline at end of file +discovery.removestale.attempts = 3 + +# Set to true to escape HTML tags in hit highlight results +discovery.highlights.escape-html = true +# Set the fields that should not escape HTML tags in hit highlight results when discovery.highlights.escape-html is true +# It is possible to provide multiple fields by separating them by commas like this: dc.description.abstract, dc.title +# discovery.highlights.html-allowed-fields = diff --git a/dspace/solr/search/conf/solrconfig.xml b/dspace/solr/search/conf/solrconfig.xml index 97b1d1ddbbf6..71c6c8846941 100644 --- a/dspace/solr/search/conf/solrconfig.xml +++ b/dspace/solr/search/conf/solrconfig.xml @@ -148,6 +148,12 @@ + + + + + + false From 6fb3a700dea12080a114521a4c8568ed599ccfcd Mon Sep 17 00:00:00 2001 From: Zahraa Chreim Date: Wed, 29 Oct 2025 15:17:01 +0200 Subject: [PATCH 384/701] 135699: Fix deletion of request items referencing deleted bitstreams and add SQL cleanup script with corresponding test --- .../dspace/content/BitstreamServiceImpl.java | 13 +++++++ ...-request-items-with-deleted-bitstreams.sql | 14 ++++++++ .../app/rest/RequestItemRepositoryIT.java | 36 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index e23e5ce2c825..0c5d12992435 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -19,6 +19,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.requestitem.RequestItem; +import org.dspace.app.requestitem.service.RequestItemService; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.dao.BitstreamDAO; @@ -63,6 +65,8 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl imp protected BundleService bundleService; @Autowired(required = true) protected BitstreamStorageService bitstreamStorageService; + @Autowired(required = true) + protected RequestItemService requestItemService; protected BitstreamServiceImpl() { super(); @@ -287,6 +291,15 @@ public void delete(Context context, Bitstream bitstream) throws SQLException, Au //Remove all bundles from the bitstream object, clearing the connection in 2 ways bundles.clear(); + // Remove any RequestItem entities associated with this bitstream ensuring there are no requests referencing + // a deleted bitstream + List requestItems = requestItemService.findAll(context); + for (RequestItem requestItem : requestItems) { + if (requestItem.getBitstream().equals(bitstream)) { + requestItemService.delete(context, requestItem); + } + } + // Remove policies only after the bitstream has been updated (otherwise the current user has not WRITE rights) authorizeService.removeAllPolicies(context, bitstream); } diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql new file mode 100644 index 000000000000..4f0c54c975c6 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql @@ -0,0 +1,14 @@ +-- +-- 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/ +-- + +DELETE +FROM requestitem +WHERE bitstream_id IN + (SELECT bs.uuid + FROM bitstream AS bs + WHERE bs.deleted IS TRUE) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java index fbbd179fd287..8495399169f9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java @@ -13,6 +13,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; @@ -58,6 +59,7 @@ import org.dspace.content.Bitstream; import org.dspace.content.Collection; import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; import org.junit.Before; @@ -655,4 +657,38 @@ public void testGetLinkTokenEmailWithoutSubPath() throws MalformedURLException, assertEquals(expectedUrl, generatedLink); configurationService.reloadConfig(); } + + /** + * Test that deleting a bitstream also removes any {@link RequestItem} entities associated with it. + */ + @Test + public void testDeleteBitstreamRemovesRequestItem() throws Exception { + // Fake up a request in REST form. + RequestItemRest rir = new RequestItemRest(); + rir.setAllfiles(false); + rir.setItemId(item.getID().toString()); + rir.setBitstreamId(bitstream.getID().toString()); + rir.setRequestEmail(eperson.getEmail()); + rir.setRequestName(eperson.getFullName()); + rir.setRequestMessage(RequestItemBuilder.REQ_MESSAGE); + + // Create it and see if it was created correctly. + ObjectMapper mapper = new ObjectMapper(); + String authToken = getAuthToken(eperson.getEmail(), password); + + getClient(authToken) + .perform(post(URI_ROOT) + .content(mapper.writeValueAsBytes(rir)) + .contentType(contentType)) + .andExpect(status().isCreated()) + // verify the body is empty + .andExpect(jsonPath("$").doesNotExist()); + + // Delete associated Bitstream + ContentServiceFactory.getInstance().getBitstreamService().delete(context, bitstream); + + // Verify that all RequestItems related to this bitstream have been removed + Iterator itemRequests = requestItemService.findByItem(context, item); + assertFalse(itemRequests.hasNext()); + } } From 5b7b0fb901d2cb1a0f8e075b9c5bf267de4c425b Mon Sep 17 00:00:00 2001 From: Zahraa Chreim Date: Wed, 29 Oct 2025 15:17:01 +0200 Subject: [PATCH 385/701] 135699: Fix deletion of request items referencing deleted bitstreams and add SQL cleanup script with corresponding test (cherry picked from commit 6fb3a700dea12080a114521a4c8568ed599ccfcd) --- .../dspace/content/BitstreamServiceImpl.java | 13 +++++++ ...-request-items-with-deleted-bitstreams.sql | 14 ++++++++ .../app/rest/RequestItemRepositoryIT.java | 36 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index bd56ad465163..7253761fd902 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -19,6 +19,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; +import org.dspace.app.requestitem.RequestItem; +import org.dspace.app.requestitem.service.RequestItemService; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.dao.BitstreamDAO; @@ -63,6 +65,8 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl imp protected BundleService bundleService; @Autowired(required = true) protected BitstreamStorageService bitstreamStorageService; + @Autowired(required = true) + protected RequestItemService requestItemService; protected BitstreamServiceImpl() { super(); @@ -287,6 +291,15 @@ public void delete(Context context, Bitstream bitstream) throws SQLException, Au //Remove all bundles from the bitstream object, clearing the connection in 2 ways bundles.clear(); + // Remove any RequestItem entities associated with this bitstream ensuring there are no requests referencing + // a deleted bitstream + List requestItems = requestItemService.findAll(context); + for (RequestItem requestItem : requestItems) { + if (requestItem.getBitstream().equals(bitstream)) { + requestItemService.delete(context, requestItem); + } + } + // Remove policies only after the bitstream has been updated (otherwise the current user has not WRITE rights) authorizeService.removeAllPolicies(context, bitstream); } diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql new file mode 100644 index 000000000000..4f0c54c975c6 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.10.29__Fix-request-items-with-deleted-bitstreams.sql @@ -0,0 +1,14 @@ +-- +-- 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/ +-- + +DELETE +FROM requestitem +WHERE bitstream_id IN + (SELECT bs.uuid + FROM bitstream AS bs + WHERE bs.deleted IS TRUE) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java index 56409d18d738..3a7c805cacc6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java @@ -13,6 +13,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -57,6 +58,7 @@ import org.dspace.content.Bitstream; import org.dspace.content.Collection; import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; import org.junit.Before; @@ -639,4 +641,38 @@ public void testGetLinkTokenEmailWithoutSubPath() throws MalformedURLException, assertEquals(expectedUrl, generatedLink); configurationService.reloadConfig(); } + + /** + * Test that deleting a bitstream also removes any {@link RequestItem} entities associated with it. + */ + @Test + public void testDeleteBitstreamRemovesRequestItem() throws Exception { + // Fake up a request in REST form. + RequestItemRest rir = new RequestItemRest(); + rir.setAllfiles(false); + rir.setItemId(item.getID().toString()); + rir.setBitstreamId(bitstream.getID().toString()); + rir.setRequestEmail(eperson.getEmail()); + rir.setRequestName(eperson.getFullName()); + rir.setRequestMessage(RequestItemBuilder.REQ_MESSAGE); + + // Create it and see if it was created correctly. + ObjectMapper mapper = new ObjectMapper(); + String authToken = getAuthToken(eperson.getEmail(), password); + + getClient(authToken) + .perform(post(URI_ROOT) + .content(mapper.writeValueAsBytes(rir)) + .contentType(contentType)) + .andExpect(status().isCreated()) + // verify the body is empty + .andExpect(jsonPath("$").doesNotExist()); + + // Delete associated Bitstream + ContentServiceFactory.getInstance().getBitstreamService().delete(context, bitstream); + + // Verify that all RequestItems related to this bitstream have been removed + Iterator itemRequests = requestItemService.findByItem(context, item); + assertFalse(itemRequests.hasNext()); + } } From 28acaf7b4a2197259aa69e27b5f39d07ab1cce12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:16:21 +0000 Subject: [PATCH 386/701] Bump org.xmlunit:xmlunit-core in the test-tools group Bumps the test-tools group with 1 update: [org.xmlunit:xmlunit-core](https://github.com/xmlunit/xmlunit). Updates `org.xmlunit:xmlunit-core` from 2.10.4 to 2.11.0 - [Release notes](https://github.com/xmlunit/xmlunit/releases) - [Changelog](https://github.com/xmlunit/xmlunit/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/xmlunit/xmlunit/compare/v2.10.4...v2.11.0) --- updated-dependencies: - dependency-name: org.xmlunit:xmlunit-core dependency-version: 2.11.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 2a14560d4f26..ac9d9a756897 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -771,7 +771,7 @@ org.xmlunit xmlunit-core - 2.10.4 + 2.11.0 test From 2a61e367ed53350b4b10b32ebea3fc3ca64e1d96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:19:54 +0000 Subject: [PATCH 387/701] Bump the spring group with 25 updates Bumps the spring group with 25 updates: | Package | From | To | | --- | --- | --- | | [org.springframework:spring-orm](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-core](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-beans](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-aop](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-context](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-context-support](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-tx](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-jdbc](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-web](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-expression](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework:spring-test](https://github.com/spring-projects/spring-framework) | `6.2.11` | `6.2.12` | | [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-tomcat](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-maven-plugin](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-cache](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-thymeleaf](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-web](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-data-rest](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-security](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-aop](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-actuator](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.boot:spring-boot-starter-log4j2](https://github.com/spring-projects/spring-boot) | `3.5.6` | `3.5.7` | | [org.springframework.security:spring-security-test](https://github.com/spring-projects/spring-security) | `6.5.5` | `6.5.6` | Updates `org.springframework:spring-orm` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-core` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-beans` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-aop` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-context` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-context-support` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-tx` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-jdbc` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-web` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-webmvc` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-expression` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-test` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-core` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-beans` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-aop` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-context` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-context-support` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-tx` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-jdbc` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-web` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-webmvc` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-expression` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework:spring-test` from 6.2.11 to 6.2.12 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.11...v6.2.12) Updates `org.springframework.boot:spring-boot-starter-test` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.security:spring-security-test` from 6.5.5 to 6.5.6 - [Release notes](https://github.com/spring-projects/spring-security/releases) - [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc) - [Commits](https://github.com/spring-projects/spring-security/compare/6.5.5...6.5.6) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.6 to 3.5.7 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.6...v3.5.7) --- updated-dependencies: - dependency-name: org.springframework:spring-orm dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.security:spring-security-test dependency-version: 6.5.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 5e2e649504a2..dfde3b377910 100644 --- a/pom.xml +++ b/pom.xml @@ -19,9 +19,9 @@ 17 - 6.2.11 - 3.5.6 - 6.5.5 + 6.2.12 + 3.5.7 + 6.5.6 6.4.10.Final 8.0.3.Final 42.7.8 From 3b032e8d828b354784486076b4319a574c3cbd73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:20:26 +0000 Subject: [PATCH 388/701] Bump com.healthmarketscience.jackcess:jackcess from 4.0.8 to 4.0.10 Bumps [com.healthmarketscience.jackcess:jackcess](https://github.com/jahlborn/jackcess) from 4.0.8 to 4.0.10. - [Commits](https://github.com/jahlborn/jackcess/compare/jackcess-4.0.8...jackcess-4.0.10) --- updated-dependencies: - dependency-name: com.healthmarketscience.jackcess:jackcess dependency-version: 4.0.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5e2e649504a2..418e7a7c14f7 100644 --- a/pom.xml +++ b/pom.xml @@ -1314,7 +1314,7 @@ com.healthmarketscience.jackcess jackcess - 4.0.8 + 4.0.10 From ca52eda42df02bcf44dcdd13ede56d52b9b3c352 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:20:31 +0000 Subject: [PATCH 389/701] Bump org.apache.bcel:bcel from 6.10.0 to 6.11.0 Bumps [org.apache.bcel:bcel](https://github.com/apache/commons-bcel) from 6.10.0 to 6.11.0. - [Changelog](https://github.com/apache/commons-bcel/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-bcel/compare/rel/commons-bcel-6.10.0...rel/commons-bcel-6.11.0) --- updated-dependencies: - dependency-name: org.apache.bcel:bcel dependency-version: 6.11.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 2a14560d4f26..592d0c29d58e 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -778,7 +778,7 @@ org.apache.bcel bcel - 6.10.0 + 6.11.0 test From d8d524045ec46f7f1bf27267fc7a41289f56843d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:20:41 +0000 Subject: [PATCH 390/701] Bump json-path.version from 2.9.0 to 2.10.0 Bumps `json-path.version` from 2.9.0 to 2.10.0. Updates `com.jayway.jsonpath:json-path` from 2.9.0 to 2.10.0 - [Release notes](https://github.com/jayway/JsonPath/releases) - [Changelog](https://github.com/json-path/JsonPath/blob/master/changelog.md) - [Commits](https://github.com/jayway/JsonPath/compare/json-path-2.9.0...json-path-2.10.0) Updates `com.jayway.jsonpath:json-path-assert` from 2.9.0 to 2.10.0 - [Release notes](https://github.com/jayway/JsonPath/releases) - [Changelog](https://github.com/json-path/JsonPath/blob/master/changelog.md) - [Commits](https://github.com/jayway/JsonPath/compare/json-path-2.9.0...json-path-2.10.0) --- updated-dependencies: - dependency-name: com.jayway.jsonpath:json-path dependency-version: 2.10.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.jayway.jsonpath:json-path-assert dependency-version: 2.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5e2e649504a2..4135eeb52e4b 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ 3.1.11 - 2.9.0 + 2.10.0 9.48 From 00240f65f96f6d661278e06f47e8cb695558e2af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:31:29 +0000 Subject: [PATCH 391/701] Bump the test-tools group with 7 updates Bumps the test-tools group with 7 updates: | Package | From | To | | --- | --- | --- | | [org.xmlunit:xmlunit-core](https://github.com/xmlunit/xmlunit) | `2.10.4` | `2.11.0` | | [io.netty:netty-buffer](https://github.com/netty/netty) | `4.2.6.Final` | `4.2.7.Final` | | [io.netty:netty-transport](https://github.com/netty/netty) | `4.2.6.Final` | `4.2.7.Final` | | [io.netty:netty-transport-native-unix-common](https://github.com/netty/netty) | `4.2.6.Final` | `4.2.7.Final` | | [io.netty:netty-common](https://github.com/netty/netty) | `4.2.6.Final` | `4.2.7.Final` | | [io.netty:netty-handler](https://github.com/netty/netty) | `4.2.6.Final` | `4.2.7.Final` | | [io.netty:netty-codec](https://github.com/netty/netty) | `4.2.6.Final` | `4.2.7.Final` | Updates `org.xmlunit:xmlunit-core` from 2.10.4 to 2.11.0 - [Release notes](https://github.com/xmlunit/xmlunit/releases) - [Changelog](https://github.com/xmlunit/xmlunit/blob/main/RELEASE_NOTES.md) - [Commits](https://github.com/xmlunit/xmlunit/compare/v2.10.4...v2.11.0) Updates `io.netty:netty-buffer` from 4.2.6.Final to 4.2.7.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.6.Final...netty-4.2.7.Final) Updates `io.netty:netty-transport` from 4.2.6.Final to 4.2.7.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.6.Final...netty-4.2.7.Final) Updates `io.netty:netty-transport-native-unix-common` from 4.2.6.Final to 4.2.7.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.6.Final...netty-4.2.7.Final) Updates `io.netty:netty-common` from 4.2.6.Final to 4.2.7.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.6.Final...netty-4.2.7.Final) Updates `io.netty:netty-handler` from 4.2.6.Final to 4.2.7.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.6.Final...netty-4.2.7.Final) Updates `io.netty:netty-codec` from 4.2.6.Final to 4.2.7.Final - [Commits](https://github.com/netty/netty/compare/netty-4.2.6.Final...netty-4.2.7.Final) --- updated-dependencies: - dependency-name: org.xmlunit:xmlunit-core dependency-version: 2.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: test-tools - dependency-name: io.netty:netty-buffer dependency-version: 4.2.7.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport dependency-version: 4.2.7.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-transport-native-unix-common dependency-version: 4.2.7.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-common dependency-version: 4.2.7.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-handler dependency-version: 4.2.7.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: io.netty:netty-codec dependency-version: 4.2.7.Final dependency-type: direct:production update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d2e3477632e8..6ffd50ddc108 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -870,32 +870,32 @@ io.netty netty-buffer - 4.2.6.Final + 4.2.7.Final io.netty netty-transport - 4.2.6.Final + 4.2.7.Final io.netty netty-transport-native-unix-common - 4.2.6.Final + 4.2.7.Final io.netty netty-common - 4.2.6.Final + 4.2.7.Final io.netty netty-handler - 4.2.6.Final + 4.2.7.Final io.netty netty-codec - 4.2.6.Final + 4.2.7.Final org.apache.velocity @@ -905,7 +905,7 @@ org.xmlunit xmlunit-core - 2.10.4 + 2.11.0 test From 6f8c2f1cd59055ef29cf3366febea7a4ad8888f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:34:29 +0000 Subject: [PATCH 392/701] Bump org.apache.bcel:bcel from 6.10.0 to 6.11.0 Bumps [org.apache.bcel:bcel](https://github.com/apache/commons-bcel) from 6.10.0 to 6.11.0. - [Changelog](https://github.com/apache/commons-bcel/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-bcel/compare/rel/commons-bcel-6.10.0...rel/commons-bcel-6.11.0) --- updated-dependencies: - dependency-name: org.apache.bcel:bcel dependency-version: 6.11.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d2e3477632e8..6214a806fdd3 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -805,7 +805,7 @@ org.apache.bcel bcel - 6.10.0 + 6.11.0 test From 3f5df043b78f7bd1f8d682dcc56aa53dc9c67028 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:34:34 +0000 Subject: [PATCH 393/701] Bump com.healthmarketscience.jackcess:jackcess from 4.0.8 to 4.0.10 Bumps [com.healthmarketscience.jackcess:jackcess](https://github.com/jahlborn/jackcess) from 4.0.8 to 4.0.10. - [Commits](https://github.com/jahlborn/jackcess/compare/jackcess-4.0.8...jackcess-4.0.10) --- updated-dependencies: - dependency-name: com.healthmarketscience.jackcess:jackcess dependency-version: 4.0.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fb4f36062d9c..067f7818ddaf 100644 --- a/pom.xml +++ b/pom.xml @@ -1318,7 +1318,7 @@ com.healthmarketscience.jackcess jackcess - 4.0.8 + 4.0.10 From b2719d6e30c25dc35d1b2a6faefc4711bfea33ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:34:53 +0000 Subject: [PATCH 394/701] Bump io.grpc:grpc-context from 1.75.0 to 1.76.0 Bumps [io.grpc:grpc-context](https://github.com/grpc/grpc-java) from 1.75.0 to 1.76.0. - [Release notes](https://github.com/grpc/grpc-java/releases) - [Commits](https://github.com/grpc/grpc-java/compare/v1.75.0...v1.76.0) --- updated-dependencies: - dependency-name: io.grpc:grpc-context dependency-version: 1.76.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fb4f36062d9c..4e97d514d9fb 100644 --- a/pom.xml +++ b/pom.xml @@ -1718,7 +1718,7 @@ io.grpc grpc-context - 1.75.0 + 1.76.0 com.google.http-client From e98c6036d646e52b1268ac37f2a418e7187d650c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 02:35:09 +0000 Subject: [PATCH 395/701] Bump json-path.version from 2.9.0 to 2.10.0 Bumps `json-path.version` from 2.9.0 to 2.10.0. Updates `com.jayway.jsonpath:json-path` from 2.9.0 to 2.10.0 - [Release notes](https://github.com/jayway/JsonPath/releases) - [Changelog](https://github.com/json-path/JsonPath/blob/master/changelog.md) - [Commits](https://github.com/jayway/JsonPath/compare/json-path-2.9.0...json-path-2.10.0) Updates `com.jayway.jsonpath:json-path-assert` from 2.9.0 to 2.10.0 - [Release notes](https://github.com/jayway/JsonPath/releases) - [Changelog](https://github.com/json-path/JsonPath/blob/master/changelog.md) - [Commits](https://github.com/jayway/JsonPath/compare/json-path-2.9.0...json-path-2.10.0) --- updated-dependencies: - dependency-name: com.jayway.jsonpath:json-path dependency-version: 2.10.0 dependency-type: direct:development update-type: version-update:semver-minor - dependency-name: com.jayway.jsonpath:json-path-assert dependency-version: 2.10.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fb4f36062d9c..0590faeb1f1a 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ - 2.9.0 + 2.10.0 7.9 From e69e9d5856ddd4dd1703a1ae9b4639a6df12c059 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Tue, 4 Nov 2025 10:16:26 +0300 Subject: [PATCH 396/701] pom.xml: adjust Jackson dependencies For some reason jackson-annotations no longer uses the patch version and jackson-databind now does. See: https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.20 --- pom.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 5e2e649504a2..278f9ccf8f31 100644 --- a/pom.xml +++ b/pom.xml @@ -30,9 +30,9 @@ 3.11.1 2.42.0 - - 2.19.2 - 2.19.2 + + 2.20.1 + 2.20 2.1.1 4.0.4 4.0.6 @@ -1731,12 +1731,12 @@ com.fasterxml classmate - 1.7.0 + 1.7.1 com.fasterxml.jackson.core jackson-annotations - ${jackson.version} + ${jackson-annotations.version} com.fasterxml.jackson.core @@ -1746,12 +1746,12 @@ com.fasterxml.jackson.core jackson-databind - ${jackson-databind.version} + ${jackson.version} com.fasterxml.jackson.datatype jackson-datatype-jsr310 - ${jackson-databind.version} + ${jackson.version} com.google.guava From c5b9565e3a98f76739867d3399055e8e2108c713 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Thu, 6 Nov 2025 10:10:00 +0300 Subject: [PATCH 397/701] pom.xml: adjust Jackson dependencies For some reason jackson-annotations no longer uses the patch version and jackson-databind now does. See: https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.20 --- pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index fb4f36062d9c..92d011ca2fc0 100644 --- a/pom.xml +++ b/pom.xml @@ -29,9 +29,9 @@ 3.11.1 2.31.0 - - 2.19.2 - 2.19.2 + + 2.20.1 + 2.20 1.3.2 2.3.1 2.3.9 @@ -1771,12 +1771,12 @@ com.fasterxml classmate - 1.7.0 + 1.7.1 com.fasterxml.jackson.core jackson-annotations - ${jackson.version} + ${jackson-annotations.version} com.fasterxml.jackson.core @@ -1786,7 +1786,7 @@ com.fasterxml.jackson.core jackson-databind - ${jackson-databind.version} + ${jackson.version} com.google.guava From 4dfbabe3cf2bfd5e705a1996e3e6372f737f4de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Tue, 21 Oct 2025 14:52:56 +0100 Subject: [PATCH 398/701] fix issue 10530 - latestForDiscovery (cherry picked from commit 95998638f9945aa3469cc2a8c4172363b9333c34) --- dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 2b388ceac7c0..470a7c7211f4 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -5,7 +5,7 @@ detailed in the LICENSE and NOTICE files at the root of the source tree and available online at - Developed by Paulo Graça + Developed by paulo-graca > https://www.openaire.eu/schema/repo-lit/4.0/openaire.xsd @@ -303,7 +303,7 @@ - From 9fee735faf9c8451b9cff68fad3e56160bf7c6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Tue, 21 Oct 2025 14:52:56 +0100 Subject: [PATCH 399/701] fix issue 10530 - latestForDiscovery (cherry picked from commit 95998638f9945aa3469cc2a8c4172363b9333c34) --- dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 8042a8cf6fd9..e9699d8e0160 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -5,7 +5,7 @@ detailed in the LICENSE and NOTICE files at the root of the source tree and available online at - Developed by Paulo Graça + Developed by paulo-graca > https://www.openaire.eu/schema/repo-lit/4.0/openaire.xsd @@ -303,7 +303,7 @@ - From 18ffbf1ffd34d37294afaa600ab2e6ea56c92dfd Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Mon, 10 Nov 2025 19:53:31 +0100 Subject: [PATCH 400/701] Fix missing assignment of Solr client in QAEventServiceImpl (cherry picked from commit 75a8cb7c7cafb6aa6f9e63ea3e47020f38b7b3ba) --- .../org/dspace/qaevent/service/impl/QAEventServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java index 98077a1c0c76..171cc4c31159 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java @@ -131,7 +131,7 @@ protected SolrClient getSolr() { if (solr == null) { String solrService = DSpaceServicesFactory.getInstance().getConfigurationService() .getProperty("qaevents.solr.server", "http://localhost:8983/solr/qaevent"); - return new HttpSolrClient.Builder(solrService).build(); + solr = new HttpSolrClient.Builder(solrService).build(); } return solr; } From 78e91bb7fc627181528a9a7b0ff9059d71754e33 Mon Sep 17 00:00:00 2001 From: im-shivamb Date: Thu, 18 Sep 2025 19:19:35 +0530 Subject: [PATCH 401/701] Fix for bitstreams with old style embargo lift date in metadata not rendering on simple item pages. (cherry picked from commit eeba9846217001e592c2f1a234ee83854485014b) --- .../src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java b/dspace-api/src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java index 7857a45eb8d5..265ec213da60 100644 --- a/dspace-api/src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java +++ b/dspace-api/src/main/java/org/dspace/embargo/DefaultEmbargoSetter.java @@ -94,7 +94,6 @@ public void setEmbargo(Context context, Item item) if (!(bnn.equals(Constants.LICENSE_BUNDLE_NAME) || bnn.equals(Constants.METADATA_BUNDLE_NAME) || bnn .equals(CreativeCommonsServiceImpl.CC_BUNDLE_NAME))) { //AuthorizeManager.removePoliciesActionFilter(context, bn, Constants.READ); - generatePolicies(context, liftDate.toDate(), null, bn, item.getOwningCollection()); for (Bitstream bs : bn.getBitstreams()) { //AuthorizeManager.removePoliciesActionFilter(context, bs, Constants.READ); generatePolicies(context, liftDate.toDate(), null, bs, item.getOwningCollection()); From dfbdf851eeadf0b06cfe8368ec21572229c1db56 Mon Sep 17 00:00:00 2001 From: Mark Patton Date: Tue, 11 Nov 2025 14:11:16 -0500 Subject: [PATCH 402/701] Port AWS SDK v2 update of S3 Bitstore Service to 8x --- dspace-api/pom.xml | 43 +- .../storage/bitstore/S3BitStoreService.java | 440 +++++++----------- .../storage/bitstore/S3BitStoreServiceIT.java | 113 ++--- dspace-server-webapp/pom.xml | 11 +- dspace/config/modules/assetstore.cfg | 17 + dspace/config/spring/api/bitstore.xml | 19 + 6 files changed, 298 insertions(+), 345 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 2a14560d4f26..483f1fc7efca 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -639,6 +639,7 @@ 1.1.1 + com.google.guava guava @@ -715,9 +716,25 @@ - com.amazonaws - aws-java-sdk-s3 - 1.12.792 + software.amazon.awssdk + s3 + 2.32.31 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + + software.amazon.awssdk.crt + aws-crt + 0.38.12 + + + + org.dspace + dspace-iiif + + org.dspace dspace-api @@ -500,10 +507,6 @@ - - org.dspace - dspace-iiif - org.dspace dspace-oai diff --git a/dspace/config/modules/assetstore.cfg b/dspace/config/modules/assetstore.cfg index 5e625e0c8a1a..74989aad0e5a 100644 --- a/dspace/config/modules/assetstore.cfg +++ b/dspace/config/modules/assetstore.cfg @@ -47,6 +47,9 @@ assetstore.s3.bucketName = # is shared. Optional, default is root level of bucket assetstore.s3.subfolder = +# Optional custom S3 endpoint URI. Leave this blank / commented to use the Amazon default +# assetstore.s3.endpoint = + # please don't use root credentials in production but rely on the aws credentials default # discovery mechanism to configure them (ENV VAR, EC2 Iam role, etc.) # The preferred approach for security reason is to use the IAM user credentials, but isn't always possible. @@ -58,3 +61,17 @@ assetstore.s3.awsSecretKey = # If the credentials are left empty, # then this setting is ignored and the default AWS region will be used. assetstore.s3.awsRegionName = + +# The target throughput for transfer requests in Gbps. Higher value means more connections will be established with S3. +assetstore.s3.targetThroughputGbps = 10.0 + +# Sets the minimum part size for transfer parts. Decreasing the minimum part size causes multipart transfer to be split +# into a larger number of smaller parts. +assetstore.s3.minPartSizeBytes = 8388608 + +# Specifies the maximum number of S3 connections that should be established during a transfer. +# If not provided, it will be based on targetThroughputGbps +assetstore.s3.maxConcurrency = + +# The algorithm the S3 client will use to create a checksum when doing putObject. +assetstore.s3.s3ChecksumAlgorithm = CRC32 diff --git a/dspace/config/spring/api/bitstore.xml b/dspace/config/spring/api/bitstore.xml index 15bb3ef1580b..af50a4c16fc3 100644 --- a/dspace/config/spring/api/bitstore.xml +++ b/dspace/config/spring/api/bitstore.xml @@ -27,6 +27,9 @@ + + + @@ -34,6 +37,22 @@ + + + + + + + + + + + + From c040729ac60aaad7956cdb851c903a5bc39b5277 Mon Sep 17 00:00:00 2001 From: Mark Patton Date: Wed, 12 Nov 2025 13:22:40 -0500 Subject: [PATCH 403/701] Port AWS SDK v2 update of S3 Bitstore Service to 7x --- dspace-api/pom.xml | 43 +- .../storage/bitstore/S3BitStoreService.java | 445 +++++++----------- .../storage/bitstore/S3BitStoreServiceIT.java | 131 +++--- dspace-server-webapp/pom.xml | 11 +- dspace/config/modules/assetstore.cfg | 17 + dspace/config/spring/api/bitstore.xml | 19 + 6 files changed, 318 insertions(+), 348 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d2e3477632e8..091efb9c6ea3 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -637,6 +637,7 @@ 1.1.1 + com.google.guava guava @@ -736,9 +737,25 @@ - com.amazonaws - aws-java-sdk-s3 - 1.12.792 + software.amazon.awssdk + s3 + 2.32.31 + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + + software.amazon.awssdk.crt + aws-crt + 0.38.12 @@ -836,22 +853,12 @@
- + - io.findify - s3mock_2.13 - 0.2.6 - test - - - com.amazonawsl - aws-java-sdk-s3 - - - com.amazonaws - aws-java-sdk-s3 - - + org.testcontainers + testcontainers-localstack + 2.0.1 + test diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index e21a101a7e98..8f7a7b728305 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -9,36 +9,20 @@ import static java.lang.String.valueOf; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Supplier; -import javax.validation.constraints.NotNull; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.regions.Region; -import com.amazonaws.regions.Regions; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.amazonaws.services.s3.model.GetObjectRequest; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.transfer.Download; -import com.amazonaws.services.s3.transfer.TransferManager; -import com.amazonaws.services.s3.transfer.TransferManagerBuilder; -import com.amazonaws.services.s3.transfer.Upload; + import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; @@ -47,7 +31,6 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.Bitstream; @@ -58,6 +41,19 @@ import org.dspace.storage.bitstore.service.BitstreamStorageService; import org.dspace.util.FunctionalUtils; import org.springframework.beans.factory.annotation.Autowired; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.async.AsyncRequestBody; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.http.HttpStatusCode; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3CrtAsyncClientBuilder; +import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; +import software.amazon.awssdk.services.s3.model.NoSuchBucketException; /** * Asset store using Amazon's Simple Storage Service (S3). @@ -66,7 +62,7 @@ * * @author Richard Rodgers, Peter Dietz * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) - * + * @author Mark Patton */ public class S3BitStoreService extends BaseBitStoreService { @@ -83,30 +79,21 @@ public class S3BitStoreService extends BaseBitStoreService { */ static final String CSA = "MD5"; - // These settings control the way an identifier is hashed into - // directory and file names - // - // With digitsPerLevel 2 and directoryLevels 3, an identifier - // like 12345678901234567890 turns into the relative name - // /12/34/56/12345678901234567890. - // - // You should not change these settings if you have data in the - // asset store, as the BitstreamStorageManager will be unable - // to find your existing data. - protected static final int digitsPerLevel = 2; - protected static final int directoryLevels = 3; - private boolean enabled = false; + /** + * Override AWS endpoint if not null + */ + private String endpoint = null; + private String awsAccessKey; private String awsSecretKey; private String awsRegionName; private boolean useRelativePath; - - /** - * The maximum size of individual chunk to download from S3 when a file is accessed. Default 5Mb - */ - private long bufferSize = 5 * 1024 * 1024; + private double targetThroughputGbps = 10.0; + private long minPartSizeBytes = 8 * 1024 * 1024L; + private ChecksumAlgorithm s3ChecksumAlgorithm = ChecksumAlgorithm.CRC32; + private Integer maxConcurrency = null; /** * container for all the assets @@ -121,13 +108,7 @@ public class S3BitStoreService extends BaseBitStoreService { /** * S3 service */ - private AmazonS3 s3Service = null; - - /** - * S3 transfer manager - * this is reused between put calls to use less resources for multiple uploads - */ - private TransferManager tm = null; + private S3AsyncClient s3AsyncClient = null; private static final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @@ -137,16 +118,42 @@ public class S3BitStoreService extends BaseBitStoreService { * * @param regions wanted regions in client * @param awsCredentials credentials of the client + * @param endpoint custom AWS endpoint + * @param targetThroughput target throughput in Gbps + * @param minPartSize minimum part size in bytes + * @param maxConcurrency maximum number of concurrent requests * @return builder with the specified parameters */ - protected static Supplier amazonClientBuilderBy( - @NotNull Regions regions, - @NotNull AWSCredentials awsCredentials + protected static Supplier amazonClientBuilderBy( + Region region, + AwsCredentialsProvider credentialsProvider, + String endpoint, + double targetThroughput, + long minPartSize, + Integer maxConcurrency ) { - return () -> AmazonS3ClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .withRegion(regions) - .build(); + return () -> { + S3CrtAsyncClientBuilder crtBuilder = S3AsyncClient.crtBuilder(); + + if (credentialsProvider != null) { + crtBuilder.credentialsProvider(credentialsProvider); + } + + if (region != null) { + crtBuilder.region(region); + } + + if (maxConcurrency != null) { + crtBuilder.maxConcurrency(maxConcurrency); + } + + if (StringUtils.isNotBlank(endpoint)) { + crtBuilder.endpointOverride(URI.create(endpoint)); + crtBuilder.forcePathStyle(true); + } + + return crtBuilder.targetThroughputInGbps(targetThroughput).minimumPartSizeInBytes(minPartSize).build(); + }; } public S3BitStoreService() {} @@ -154,10 +161,10 @@ public S3BitStoreService() {} /** * This constructor is used for test purpose. * - * @param s3Service AmazonS3 service + * @param s3AsyncClient AmazonS3 service */ - protected S3BitStoreService(AmazonS3 s3Service) { - this.s3Service = s3Service; + protected S3BitStoreService(S3AsyncClient s3AsyncClient) { + this.s3AsyncClient = s3AsyncClient; } @Override @@ -174,7 +181,6 @@ public boolean isEnabled() { */ @Override public void init() throws IOException { - if (this.isInitialized() || !this.isEnabled()) { return; } @@ -183,29 +189,31 @@ public void init() throws IOException { if (StringUtils.isNotBlank(getAwsAccessKey()) && StringUtils.isNotBlank(getAwsSecretKey())) { log.warn("Use local defined S3 credentials"); // region - Regions regions = Regions.DEFAULT_REGION; + Region region = Region.US_EAST_1; if (StringUtils.isNotBlank(awsRegionName)) { try { - regions = Regions.fromName(awsRegionName); + region = Region.of(awsRegionName); } catch (IllegalArgumentException e) { log.warn("Invalid aws_region: " + awsRegionName); } } + // init client - s3Service = FunctionalUtils.getDefaultOrBuild( - this.s3Service, + s3AsyncClient = FunctionalUtils.getDefaultOrBuild( + this.s3AsyncClient, amazonClientBuilderBy( - regions, - new BasicAWSCredentials(getAwsAccessKey(), getAwsSecretKey()) - ) + region, + StaticCredentialsProvider.create(AwsBasicCredentials.create(getAwsAccessKey(), + getAwsSecretKey())), endpoint, targetThroughputGbps, + minPartSizeBytes, maxConcurrency) ); - log.warn("S3 Region set to: " + regions.getName()); + log.warn("S3 Region set to: " + region.id()); } else { log.info("Using a IAM role or aws environment credentials"); - s3Service = FunctionalUtils.getDefaultOrBuild( - this.s3Service, - AmazonS3ClientBuilder::defaultClient - ); + s3AsyncClient = FunctionalUtils.getDefaultOrBuild( + this.s3AsyncClient, + amazonClientBuilderBy(null, null , endpoint, targetThroughputGbps, + minPartSizeBytes, maxConcurrency)); } // bucket name @@ -216,13 +224,10 @@ public void init() throws IOException { log.warn("S3 BucketName is not configured, setting default: " + bucketName); } - try { - if (!s3Service.doesBucketExistV2(bucketName)) { - s3Service.createBucket(bucketName); - log.info("Creating new S3 Bucket: " + bucketName); - } - } catch (AmazonClientException e) { - throw new IOException(e); + + if (!doesBucketExist(bucketName)) { + s3AsyncClient.createBucket(r -> r.bucket(bucketName)).join(); + log.info("Creating new S3 Bucket: " + bucketName); } this.initialized = true; log.info("AWS S3 Assetstore ready to go! bucket:" + bucketName); @@ -230,13 +235,23 @@ public void init() throws IOException { this.initialized = false; log.error("Can't initialize this store!", e); } + } - log.info("AWS S3 Assetstore ready to go! bucket:" + bucketName); + /** + * @param bucketName + * @return whether or not the specified bucket exists + */ + public boolean doesBucketExist(String bucketName ) { + try { + s3AsyncClient.headBucket(r -> r.bucket(bucketName)).join(); + return true; + } catch (CompletionException ce) { + if (!(ce.getCause() instanceof NoSuchBucketException)) { + log.error("headBucket(" + bucketName + ")", ce.getCause()); + } - tm = FunctionalUtils.getDefaultOrBuild(tm, () -> TransferManagerBuilder.standard() - .withAlwaysCalculateMultipartMd5(true) - .withS3Client(s3Service) - .build()); + return false; + } } /** @@ -264,7 +279,15 @@ public InputStream get(Bitstream bitstream) throws IOException { if (isRegisteredBitstream(key)) { key = key.substring(REGISTERED_FLAG.length()); } - return new S3LazyInputStream(key, bufferSize, bitstream.getSizeBytes()); + + final String objectKey = key; + + try { + return s3AsyncClient.getObject(r -> r.bucket(bucketName).key(objectKey), + AsyncResponseTransformer.toBlockingInputStream()).join(); + } catch (CompletionException e) { + throw new IOException(e.getCause()); + } } /** @@ -281,44 +304,40 @@ public InputStream get(Bitstream bitstream) throws IOException { @Override public void put(Bitstream bitstream, InputStream in) throws IOException { String key = getFullKey(bitstream.getInternalId()); - //Copy istream to temp file, and send the file, with some metadata - File scratchFile = File.createTempFile(bitstream.getInternalId(), "s3bs"); - try ( - FileOutputStream fos = new FileOutputStream(scratchFile); - // Read through a digest input stream that will work out the MD5 - DigestInputStream dis = new DigestInputStream(in, MessageDigest.getInstance(CSA)); - ) { - Utils.bufferedCopy(dis, fos); - in.close(); + ExecutorService executor = Executors.newSingleThreadExecutor(); - Upload upload = tm.upload(bucketName, key, scratchFile); + try (DigestInputStream dis = new DigestInputStream(in, MessageDigest.getInstance(CSA))) { + AsyncRequestBody body = AsyncRequestBody.fromInputStream(dis, null, executor); - upload.waitForUploadResult(); + s3AsyncClient.putObject(b -> b.bucket(bucketName).key(key).checksumAlgorithm(s3ChecksumAlgorithm), + body).join(); + + bitstream.setSizeBytes(s3AsyncClient.headObject(r -> r.bucket(bucketName).key(key)) + .join().contentLength()); - bitstream.setSizeBytes(scratchFile.length()); // we cannot use the S3 ETAG here as it could be not a MD5 in case of multipart upload (large files) or if // the bucket is encrypted bitstream.setChecksum(Utils.toHex(dis.getMessageDigest().digest())); bitstream.setChecksumAlgorithm(CSA); - - } catch (AmazonClientException | IOException | InterruptedException e) { + } catch (CompletionException e) { + log.error("put(" + bitstream.getInternalId() + ", is)", e.getCause()); + throw new IOException(e.getCause()); + } catch (IOException e) { log.error("put(" + bitstream.getInternalId() + ", is)", e); throw new IOException(e); } catch (NoSuchAlgorithmException nsae) { // Should never happen log.warn("Caught NoSuchAlgorithmException", nsae); } finally { - if (!scratchFile.delete()) { - scratchFile.deleteOnExit(); - } + executor.shutdown(); + in.close(); } } /** * Obtain technical metadata about an asset in the asset store. * - * Checksum used is (ETag) hex encoded 128-bit MD5 digest of an object's content as calculated by Amazon S3 - * (Does not use getContentMD5, as that is 128-bit MD5 digest calculated on caller's side) + * The MD5 checksum is calculated locally because it is not supported by AWS. * * @param bitstream The asset to describe * @param attrs A List of desired metadata fields @@ -329,7 +348,6 @@ public void put(Bitstream bitstream, InputStream in) throws IOException { */ @Override public Map about(Bitstream bitstream, List attrs) throws IOException { - String key = getFullKey(bitstream.getInternalId()); // If this is a registered bitstream, strip the -R prefix before retrieving if (isRegisteredBitstream(key)) { @@ -339,20 +357,18 @@ public Map about(Bitstream bitstream, List attrs) throws Map metadata = new HashMap<>(); try { + final String objectKey = key; + HeadObjectResponse response = s3AsyncClient.headObject(r -> r.bucket(bucketName).key(objectKey)).join(); - ObjectMetadata objectMetadata = s3Service.getObjectMetadata(bucketName, key); - if (objectMetadata != null) { - putValueIfExistsKey(attrs, metadata, "size_bytes", objectMetadata.getContentLength()); - putValueIfExistsKey(attrs, metadata, "modified", valueOf(objectMetadata.getLastModified().getTime())); - } - + putValueIfExistsKey(attrs, metadata, "size_bytes", response.contentLength()); + putValueIfExistsKey(attrs, metadata, "modified", valueOf(response.lastModified().toEpochMilli())); putValueIfExistsKey(attrs, metadata, "checksum_algorithm", CSA); if (attrs.contains("checksum")) { try (InputStream in = get(bitstream); DigestInputStream dis = new DigestInputStream(in, MessageDigest.getInstance(CSA)) ) { - Utils.copy(dis, NullOutputStream.NULL_OUTPUT_STREAM); + Utils.copy(dis, NullOutputStream.INSTANCE); byte[] md5Digest = dis.getMessageDigest().digest(); metadata.put("checksum", Utils.toHex(md5Digest)); } catch (NoSuchAlgorithmException nsae) { @@ -362,15 +378,16 @@ public Map about(Bitstream bitstream, List attrs) throws } return metadata; - } catch (AmazonS3Exception e) { - if (e.getStatusCode() == HttpStatus.SC_NOT_FOUND) { - return metadata; + } catch (CompletionException e) { + if (e.getCause() instanceof AwsServiceException) { + if (((AwsServiceException)e.getCause()).statusCode() == HttpStatusCode.NOT_FOUND) { + return metadata; + } } - } catch (AmazonClientException e) { + log.error("about(" + key + ", attrs)", e); throw new IOException(e); } - return metadata; } /** @@ -383,10 +400,10 @@ public Map about(Bitstream bitstream, List attrs) throws public void remove(Bitstream bitstream) throws IOException { String key = getFullKey(bitstream.getInternalId()); try { - s3Service.deleteObject(bucketName, key); - } catch (AmazonClientException e) { - log.error("remove(" + key + ")", e); - throw new IOException(e); + s3AsyncClient.deleteObject(r -> r.bucket(bucketName).key(key)).join(); + } catch (CompletionException e) { + log.error("remove(" + key + ")", e.getCause()); + throw new IOException(e.getCause()); } } @@ -497,6 +514,46 @@ public void setUseRelativePath(boolean useRelativePath) { this.useRelativePath = useRelativePath; } + public double getTargetThroughputGbps() { + return targetThroughputGbps; + } + + public void setTargetThroughputGbps(double targetThroughputGbps) { + this.targetThroughputGbps = targetThroughputGbps; + } + + public long getMinPartSizeBytes() { + return minPartSizeBytes; + } + + public void setMinPartSizeBytes(long minPartSizeBytes) { + this.minPartSizeBytes = minPartSizeBytes; + } + + public ChecksumAlgorithm getS3ChecksumAlgorithm() { + return s3ChecksumAlgorithm; + } + + public void setS3ChecksumAlgorithm(ChecksumAlgorithm s3ChecksumAlgorithm) { + this.s3ChecksumAlgorithm = s3ChecksumAlgorithm; + } + + public Integer getMaxConcurrency() { + return maxConcurrency; + } + + public void setMaxConcurrency(Integer maxConcurrency) { + this.maxConcurrency = maxConcurrency; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + /** * Contains a command-line testing tool. Expects arguments: * -a accessKey -s secretKey -f assetFileName @@ -537,73 +594,18 @@ public static void main(String[] args) throws Exception { S3BitStoreService store = new S3BitStoreService(); - AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); - - store.s3Service = AmazonS3ClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .build(); + StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey)); - //Todo configurable region - Region usEast1 = Region.getRegion(Regions.US_EAST_1); - store.s3Service.setRegion(usEast1); + // Todo configurable region + store.s3AsyncClient = S3AsyncClient.builder().credentialsProvider(credentialsProvider). + region(Region.US_EAST_1).build(); // get hostname of DSpace UI to use to name bucket String hostname = Utils.getHostName(configurationService.getProperty("dspace.ui.url")); //Bucketname should be lowercase store.bucketName = DEFAULT_BUCKET_PREFIX + hostname + ".s3test"; - store.s3Service.createBucket(store.bucketName); - /* Broken in DSpace 6 TODO Refactor - // time everything, todo, swtich to caliper - long start = System.currentTimeMillis(); - // Case 1: store a file - String id = store.generateId(); - System.out.print("put() file " + assetFile + " under ID " + id + ": "); - FileInputStream fis = new FileInputStream(assetFile); - //TODO create bitstream for assetfile... - Map attrs = store.put(fis, id); - long now = System.currentTimeMillis(); - System.out.println((now - start) + " msecs"); - start = now; - // examine the metadata returned - Iterator iter = attrs.keySet().iterator(); - System.out.println("Metadata after put():"); - while (iter.hasNext()) - { - String key = (String)iter.next(); - System.out.println( key + ": " + (String)attrs.get(key) ); - } - // Case 2: get metadata and compare - System.out.print("about() file with ID " + id + ": "); - Map attrs2 = store.about(id, attrs); - now = System.currentTimeMillis(); - System.out.println((now - start) + " msecs"); - start = now; - iter = attrs2.keySet().iterator(); - System.out.println("Metadata after about():"); - while (iter.hasNext()) - { - String key = (String)iter.next(); - System.out.println( key + ": " + (String)attrs.get(key) ); - } - // Case 3: retrieve asset and compare bits - System.out.print("get() file with ID " + id + ": "); - java.io.FileOutputStream fos = new java.io.FileOutputStream(assetFile+".echo"); - InputStream in = store.get(id); - Utils.bufferedCopy(in, fos); - fos.close(); - in.close(); - now = System.currentTimeMillis(); - System.out.println((now - start) + " msecs"); - start = now; - // Case 4: remove asset - System.out.print("remove() file with ID: " + id + ": "); - store.remove(id); - now = System.currentTimeMillis(); - System.out.println((now - start) + " msecs"); - System.out.flush(); - // should get nothing back now - will throw exception - store.get(id); -*/ + store.s3AsyncClient.createBucket(r -> r.bucket(store.bucketName)).join(); } /** @@ -614,85 +616,4 @@ public static void main(String[] args) throws Exception { public boolean isRegisteredBitstream(String internalId) { return internalId.startsWith(REGISTERED_FLAG); } - - public void setBufferSize(long bufferSize) { - this.bufferSize = bufferSize; - } - - /** - * This inner class represent an InputStream that uses temporary files to - * represent chunk of the object downloaded from S3. When the input stream is - * read the class look first to the current chunk and download a new one once if - * the current one as been fully read. The class is responsible to close a chunk - * as soon as a new one is retrieved, the last chunk is closed when the input - * stream itself is closed or the last byte is read (the first of the two) - */ - public class S3LazyInputStream extends InputStream { - private InputStream currentChunkStream; - private String objectKey; - private long endOfChunk = -1; - private long chunkMaxSize; - private long currPos = 0; - private long fileSize; - - public S3LazyInputStream(String objectKey, long chunkMaxSize, long fileSize) throws IOException { - this.objectKey = objectKey; - this.chunkMaxSize = chunkMaxSize; - this.endOfChunk = 0; - this.fileSize = fileSize; - downloadChunk(); - } - - @Override - public int read() throws IOException { - // is the current chunk completely read and other are available? - if (currPos == endOfChunk && currPos < fileSize) { - currentChunkStream.close(); - downloadChunk(); - } - - int byteRead = currPos < endOfChunk ? currentChunkStream.read() : -1; - // do we get any data or are we at the end of the file? - if (byteRead != -1) { - currPos++; - } else { - currentChunkStream.close(); - } - return byteRead; - } - - /** - * This method download the next chunk from S3 - * - * @throws IOException - * @throws FileNotFoundException - */ - private void downloadChunk() throws IOException, FileNotFoundException { - // Create a DownloadFileRequest with the desired byte range - long startByte = currPos; // Start byte (inclusive) - long endByte = Long.min(startByte + chunkMaxSize - 1, fileSize - 1); // End byte (inclusive) - GetObjectRequest getRequest = new GetObjectRequest(bucketName, objectKey) - .withRange(startByte, endByte); - - File currentChunkFile = File.createTempFile("s3-disk-copy-" + UUID.randomUUID(), "temp"); - currentChunkFile.deleteOnExit(); - try { - Download download = tm.download(getRequest, currentChunkFile); - download.waitForCompletion(); - currentChunkStream = new DeleteOnCloseFileInputStream(currentChunkFile); - endOfChunk = endOfChunk + download.getProgress().getBytesTransferred(); - } catch (AmazonClientException | InterruptedException e) { - currentChunkFile.delete(); - throw new IOException(e); - } - } - - @Override - public void close() throws IOException { - if (currentChunkStream != null) { - currentChunkStream.close(); - } - } - - } } diff --git a/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java b/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java index 63d88e950116..e2f68baf3aec 100644 --- a/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java @@ -7,13 +7,12 @@ */ package org.dspace.storage.bitstore; -import static com.amazonaws.regions.Regions.DEFAULT_REGION; import static java.nio.charset.StandardCharsets.UTF_8; import static org.dspace.storage.bitstore.S3BitStoreService.CSA; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; @@ -24,8 +23,10 @@ import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.PrintWriter; import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -34,15 +35,6 @@ import java.util.List; import java.util.Map; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.AnonymousAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.AmazonS3Exception; -import com.amazonaws.services.s3.model.Bucket; -import com.amazonaws.services.s3.model.ObjectMetadata; -import io.findify.s3mock.S3Mock; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.BooleanUtils; import org.dspace.AbstractIntegrationTestWithDatabase; @@ -60,44 +52,69 @@ import org.dspace.services.factory.DSpaceServicesFactory; import org.hamcrest.Matcher; import org.hamcrest.Matchers; -import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; - - +import org.testcontainers.localstack.LocalStackContainer; +import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.Bucket; +import software.amazon.awssdk.services.s3.model.ChecksumAlgorithm; +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; /** * @author Luca Giamminonni (luca.giamminonni at 4science.com) */ public class S3BitStoreServiceIT extends AbstractIntegrationTestWithDatabase { + private static DockerImageName localstackName = DockerImageName.parse("localstack/localstack:stable"); - private static final String DEFAULT_BUCKET_NAME = "dspace-asset-localhost"; + @SuppressWarnings("resource") + private static LocalStackContainer localstackContainer = new LocalStackContainer(localstackName).withServices("s3"); - private S3BitStoreService s3BitStoreService; + private static S3AsyncClient s3AsyncClient; - private AmazonS3 amazonS3Client; + private static final String DEFAULT_BUCKET_NAME = "dspace-asset-localhost"; - private S3Mock s3Mock; + private S3BitStoreService s3BitStoreService; private Collection collection; private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + @BeforeClass + public static void setupS3() { + localstackContainer.start(); + + s3AsyncClient = S3AsyncClient.crtBuilder() + .endpointOverride(localstackContainer.getEndpoint()) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(localstackContainer.getAccessKey(), + localstackContainer.getSecretKey()) + )) + .region(Region.of(localstackContainer.getRegion())) + .build(); + } + + @AfterClass + public static void cleanupS3() { + localstackContainer.close(); + s3AsyncClient.close(); + } @Before public void setup() throws Exception { - configurationService.setProperty("assetstore.s3.enabled", "true"); - s3Mock = new S3Mock.Builder().withPort(8001).withInMemoryBackend().build(); - s3Mock.start(); - - amazonS3Client = createAmazonS3Client(); - - s3BitStoreService = new S3BitStoreService(amazonS3Client); + s3BitStoreService = new S3BitStoreService(s3AsyncClient); s3BitStoreService.setEnabled(BooleanUtils.toBoolean( configurationService.getProperty("assetstore.s3.enabled"))); - s3BitStoreService.setBufferSize(22); + s3BitStoreService.setS3ChecksumAlgorithm(ChecksumAlgorithm.SHA256); + context.turnOffAuthorisationSystem(); parentCommunity = CommunityBuilder.createCommunity(context) @@ -109,22 +126,17 @@ public void setup() throws Exception { context.restoreAuthSystemState(); } - @After - public void cleanUp() { - s3Mock.shutdown(); - } - @Test public void testBitstreamPutAndGetWithAlreadyPresentBucket() throws IOException { String bucketName = "testbucket"; - amazonS3Client.createBucket(bucketName); + s3AsyncClient.createBucket(r -> r.bucket(bucketName)).join(); s3BitStoreService.setBucketName(bucketName); s3BitStoreService.init(); - assertThat(amazonS3Client.listBuckets(), contains(bucketNamed(bucketName))); + assertThat(s3AsyncClient.listBuckets().join().buckets(), hasItem(bucketNamed(bucketName))); context.turnOffAuthorisationSystem(); String content = "Test bitstream content"; @@ -146,7 +158,7 @@ public void testBitstreamPutAndGetWithAlreadyPresentBucket() throws IOException private void checkGetPut(String bucketName, String content, Bitstream bitstream) throws IOException { s3BitStoreService.put(bitstream, toInputStream(content)); - String expectedChecksum = Utils.toHex(generateChecksum(content)); + String expectedChecksum = Utils.toHex(generateChecksum("MD5", content)); assertThat(bitstream.getSizeBytes(), is((long) content.length())); assertThat(bitstream.getChecksum(), is(expectedChecksum)); @@ -154,20 +166,16 @@ private void checkGetPut(String bucketName, String content, Bitstream bitstream) InputStream inputStream = s3BitStoreService.get(bitstream); assertThat(IOUtils.toString(inputStream, UTF_8), is(content)); - - String key = s3BitStoreService.getFullKey(bitstream.getInternalId()); - ObjectMetadata objectMetadata = amazonS3Client.getObjectMetadata(bucketName, key); - assertThat(objectMetadata.getContentMD5(), is(expectedChecksum)); } @Test - public void testBitstreamPutAndGetWithoutSpecifingBucket() throws IOException { + public void testBitstreamPutAndGetWithoutSpecifyingBucket() throws IOException { s3BitStoreService.init(); assertThat(s3BitStoreService.getBucketName(), is(DEFAULT_BUCKET_NAME)); - assertThat(amazonS3Client.listBuckets(), contains(bucketNamed(DEFAULT_BUCKET_NAME))); + assertThat(s3AsyncClient.listBuckets().join().buckets(), hasItem(bucketNamed(DEFAULT_BUCKET_NAME))); context.turnOffAuthorisationSystem(); String content = "Test bitstream content"; @@ -176,7 +184,7 @@ public void testBitstreamPutAndGetWithoutSpecifingBucket() throws IOException { s3BitStoreService.put(bitstream, toInputStream(content)); - String expectedChecksum = Utils.toHex(generateChecksum(content)); + String expectedChecksum = Utils.toHex(generateChecksum("MD5", content)); assertThat(bitstream.getSizeBytes(), is((long) content.length())); assertThat(bitstream.getChecksum(), is(expectedChecksum)); @@ -184,11 +192,6 @@ public void testBitstreamPutAndGetWithoutSpecifingBucket() throws IOException { InputStream inputStream = s3BitStoreService.get(bitstream); assertThat(IOUtils.toString(inputStream, UTF_8), is(content)); - - String key = s3BitStoreService.getFullKey(bitstream.getInternalId()); - ObjectMetadata objectMetadata = amazonS3Client.getObjectMetadata(DEFAULT_BUCKET_NAME, key); - assertThat(objectMetadata.getContentMD5(), is(expectedChecksum)); - } @Test @@ -210,9 +213,9 @@ public void testBitstreamPutAndGetWithSubFolder() throws IOException { String key = s3BitStoreService.getFullKey(bitstream.getInternalId()); assertThat(key, startsWith("test/DSpace7/")); - ObjectMetadata objectMetadata = amazonS3Client.getObjectMetadata(DEFAULT_BUCKET_NAME, key); - assertThat(objectMetadata, notNullValue()); - + HeadObjectResponse response = s3AsyncClient.headObject(r -> + r.bucket(DEFAULT_BUCKET_NAME).key(key)).join(); + assertThat(response, notNullValue()); } @Test @@ -232,8 +235,8 @@ public void testBitstreamDeletion() throws IOException { s3BitStoreService.remove(bitstream); IOException exception = assertThrows(IOException.class, () -> s3BitStoreService.get(bitstream)); - assertThat(exception.getCause(), instanceOf(AmazonS3Exception.class)); - assertThat(((AmazonS3Exception) exception.getCause()).getStatusCode(), is(404)); + assertThat(exception.getCause(), instanceOf(AwsServiceException.class)); + assertThat(((AwsServiceException) exception.getCause()).statusCode(), is(404)); } @@ -253,6 +256,14 @@ public void testAbout() throws IOException { assertThat(about.size(), is(0)); about = s3BitStoreService.about(bitstream, List.of("size_bytes")); + + { + PrintWriter out = new PrintWriter(new FileWriter("/tmp/about.txt")); + out.println("moo"); + out.println(about); + out.close(); + } + assertThat(about, hasEntry("size_bytes", 22L)); assertThat(about.size(), is(1)); @@ -261,7 +272,7 @@ public void testAbout() throws IOException { assertThat(about, hasEntry(is("modified"), notNullValue())); assertThat(about.size(), is(2)); - String expectedChecksum = Utils.toHex(generateChecksum(content)); + String expectedChecksum = Utils.toHex(generateChecksum("MD5", content)); about = s3BitStoreService.about(bitstream, List.of("size_bytes", "modified", "checksum")); assertThat(about, hasEntry("size_bytes", 22L)); @@ -275,7 +286,6 @@ public void testAbout() throws IOException { assertThat(about, hasEntry("checksum", expectedChecksum)); assertThat(about, hasEntry("checksum_algorithm", CSA)); assertThat(about.size(), is(4)); - } @Test @@ -406,16 +416,16 @@ public void givenBitStreamIdentifierWithSlashesWhenSanitizedThenSlashesMustBeRem public void testDoNotInitializeConfigured() throws Exception { String assetstores3enabledOldValue = configurationService.getProperty("assetstore.s3.enabled"); configurationService.setProperty("assetstore.s3.enabled", "false"); - s3BitStoreService = new S3BitStoreService(amazonS3Client); + s3BitStoreService = new S3BitStoreService(s3AsyncClient); s3BitStoreService.init(); assertFalse(s3BitStoreService.isInitialized()); assertFalse(s3BitStoreService.isEnabled()); configurationService.setProperty("assetstore.s3.enabled", assetstores3enabledOldValue); } - private byte[] generateChecksum(String content) { + private byte[] generateChecksum(String algorithm, String content) { try { - MessageDigest m = MessageDigest.getInstance("MD5"); + MessageDigest m = MessageDigest.getInstance(algorithm); m.update(content.getBytes()); return m.digest(); } catch (NoSuchAlgorithmException e) { @@ -423,13 +433,6 @@ private byte[] generateChecksum(String content) { } } - private AmazonS3 createAmazonS3Client() { - return AmazonS3ClientBuilder.standard() - .withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials())) - .withEndpointConfiguration(new EndpointConfiguration("http://127.0.0.1:8001", DEFAULT_REGION.getName())) - .build(); - } - private Item createItem() { return ItemBuilder.createItem(context, collection) .withTitle("Test item") @@ -447,7 +450,7 @@ private Bitstream createBitstream(String content) { } private Matcher bucketNamed(String name) { - return LambdaMatcher.matches(bucket -> bucket.getName().equals(name)); + return LambdaMatcher.matches(bucket -> bucket.name().equals(name)); } private InputStream toInputStream(String content) { diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 84227b4a12fb..0f03a7335c24 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -439,6 +439,13 @@ + + + + org.dspace + dspace-iiif + + org.dspace dspace-api @@ -458,10 +465,6 @@ - - org.dspace - dspace-iiif - org.dspace dspace-oai diff --git a/dspace/config/modules/assetstore.cfg b/dspace/config/modules/assetstore.cfg index 5e625e0c8a1a..74989aad0e5a 100644 --- a/dspace/config/modules/assetstore.cfg +++ b/dspace/config/modules/assetstore.cfg @@ -47,6 +47,9 @@ assetstore.s3.bucketName = # is shared. Optional, default is root level of bucket assetstore.s3.subfolder = +# Optional custom S3 endpoint URI. Leave this blank / commented to use the Amazon default +# assetstore.s3.endpoint = + # please don't use root credentials in production but rely on the aws credentials default # discovery mechanism to configure them (ENV VAR, EC2 Iam role, etc.) # The preferred approach for security reason is to use the IAM user credentials, but isn't always possible. @@ -58,3 +61,17 @@ assetstore.s3.awsSecretKey = # If the credentials are left empty, # then this setting is ignored and the default AWS region will be used. assetstore.s3.awsRegionName = + +# The target throughput for transfer requests in Gbps. Higher value means more connections will be established with S3. +assetstore.s3.targetThroughputGbps = 10.0 + +# Sets the minimum part size for transfer parts. Decreasing the minimum part size causes multipart transfer to be split +# into a larger number of smaller parts. +assetstore.s3.minPartSizeBytes = 8388608 + +# Specifies the maximum number of S3 connections that should be established during a transfer. +# If not provided, it will be based on targetThroughputGbps +assetstore.s3.maxConcurrency = + +# The algorithm the S3 client will use to create a checksum when doing putObject. +assetstore.s3.s3ChecksumAlgorithm = CRC32 diff --git a/dspace/config/spring/api/bitstore.xml b/dspace/config/spring/api/bitstore.xml index 15bb3ef1580b..af50a4c16fc3 100644 --- a/dspace/config/spring/api/bitstore.xml +++ b/dspace/config/spring/api/bitstore.xml @@ -27,6 +27,9 @@ + + + @@ -34,6 +37,22 @@ + + + + + + + + + + + + From 418be780e2bf5fd113b2cc49381d0b2dd6bb2288 Mon Sep 17 00:00:00 2001 From: igorbaptist4 Date: Wed, 29 Oct 2025 17:10:49 -0300 Subject: [PATCH 404/701] fix: #8443 - Remove the step 'complete' and fix the processing-class for step 'collection' (cherry picked from commit 677c7b5af6d070bd95f667a4cac24ed1f8b28644) --- dspace/config/migration/item-submissions.xsl | 61 +++++++++++--------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/dspace/config/migration/item-submissions.xsl b/dspace/config/migration/item-submissions.xsl index 9b1de738e1fd..aed95992ab17 100644 --- a/dspace/config/migration/item-submissions.xsl +++ b/dspace/config/migration/item-submissions.xsl @@ -34,35 +34,44 @@ configuration file into a DSpace 7.x (or above) item-submission.xml --> - - - - - - - - - - - + + + + + + + + + + + + org.dspace.app.rest.submit.step.CollectionStep + + + + + + + + + + + + + submission-form + + + - - + + submission + + + submission - - submission-form - - - - - submission - - - submission - - - + + From b84a8a620f19c271b1d97291ca3d5a47d93959a0 Mon Sep 17 00:00:00 2001 From: igorbaptist4 Date: Wed, 29 Oct 2025 17:10:49 -0300 Subject: [PATCH 405/701] fix: #8443 - Remove the step 'complete' and fix the processing-class for step 'collection' (cherry picked from commit 677c7b5af6d070bd95f667a4cac24ed1f8b28644) --- dspace/config/migration/item-submissions.xsl | 61 +++++++++++--------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/dspace/config/migration/item-submissions.xsl b/dspace/config/migration/item-submissions.xsl index 9b1de738e1fd..aed95992ab17 100644 --- a/dspace/config/migration/item-submissions.xsl +++ b/dspace/config/migration/item-submissions.xsl @@ -34,35 +34,44 @@ configuration file into a DSpace 7.x (or above) item-submission.xml --> - - - - - - - - - - - + + + + + + + + + + + + org.dspace.app.rest.submit.step.CollectionStep + + + + + + + + + + + + + submission-form + + + - - + + submission + + + submission - - submission-form - - - - - submission - - - submission - - - + + From 7af27de2055f7bf3c96a2f3e5f0da1d995d972b6 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 12 Nov 2025 16:54:32 -0600 Subject: [PATCH 406/701] Switch to installing regctl manually instead of using a plugin (cherry picked from commit 8954575405fd9b77d719313addfe1bf997356147) --- .github/workflows/reusable-docker-build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index a3729c27f6ee..de2831f8ff54 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -78,6 +78,8 @@ env: # Registry used during building of Docker images. (All images are later copied to docker.io registry) # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. DOCKER_BUILD_REGISTRY: ghcr.io + # Version of 'regctl' to use for copying images to DOCKER_BUILD_REGISTRY + REGCTL_VERSION: 'v0.9.2' jobs: docker-build: @@ -298,9 +300,9 @@ jobs: # 'regctl' is used to more easily copy the image to DockerHub and obtain the digest from DockerHub # See https://github.com/regclient/regclient/blob/main/docs/regctl.md - name: Install regctl for Docker registry tools - uses: regclient/actions/regctl-installer@main - with: - release: 'v0.8.0' + run: | + curl -L https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64 > /usr/bin/regctl + chmod 755 /usr/bin/regctl # This recreates Docker tags for DockerHub - name: Add Docker metadata for image From 12d56d7ef3f170f86aedabb2122f423691a8e6f2 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 12 Nov 2025 17:08:03 -0600 Subject: [PATCH 407/701] Fix bug in 11541 : Ensure regctl is added to GitHub path rather than installing in a directory that is access restricted --- .github/workflows/reusable-docker-build.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index de2831f8ff54..5e011c4668ed 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -78,8 +78,6 @@ env: # Registry used during building of Docker images. (All images are later copied to docker.io registry) # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. DOCKER_BUILD_REGISTRY: ghcr.io - # Version of 'regctl' to use for copying images to DOCKER_BUILD_REGISTRY - REGCTL_VERSION: 'v0.9.2' jobs: docker-build: @@ -301,8 +299,11 @@ jobs: # See https://github.com/regclient/regclient/blob/main/docs/regctl.md - name: Install regctl for Docker registry tools run: | - curl -L https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64 > /usr/bin/regctl - chmod 755 /usr/bin/regctl + export REGCTL_VERSION=v0.9.2 + mkdir -p bin + curl -sSLo bin/regctl https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64 + chmod a+x bin/regctl + echo "$(pwd)/bin" >> $GITHUB_PATH # This recreates Docker tags for DockerHub - name: Add Docker metadata for image From 37633a0e8ff4906d9343abb57a60158377fc5ee9 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 12 Nov 2025 16:54:32 -0600 Subject: [PATCH 408/701] Switch to installing regctl manually instead of using a plugin --- .github/workflows/reusable-docker-build.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 8165fe7f8400..c7c42edd30fa 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -79,6 +79,8 @@ env: # Registry used during building of Docker images. (All images are later copied to docker.io registry) # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. DOCKER_BUILD_REGISTRY: ghcr.io + # Version of 'regctl' to use for copying images to DOCKER_BUILD_REGISTRY + REGCTL_VERSION: 'v0.9.2' jobs: docker-build: @@ -299,9 +301,9 @@ jobs: # 'regctl' is used to more easily copy the image to DockerHub and obtain the digest from DockerHub # See https://github.com/regclient/regclient/blob/main/docs/regctl.md - name: Install regctl for Docker registry tools - uses: regclient/actions/regctl-installer@main - with: - release: 'v0.8.0' + run: | + curl -L https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64 > /usr/bin/regctl + chmod 755 /usr/bin/regctl # This recreates Docker tags for DockerHub - name: Add Docker metadata for image From 68e66c5544cce89924319a87607d56497304d15b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 12 Nov 2025 17:08:03 -0600 Subject: [PATCH 409/701] Fix bug in 11541 : Ensure regctl is added to GitHub path rather than installing in a directory that is access restricted --- .github/workflows/reusable-docker-build.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index c7c42edd30fa..86da7b9788ff 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -79,8 +79,6 @@ env: # Registry used during building of Docker images. (All images are later copied to docker.io registry) # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. DOCKER_BUILD_REGISTRY: ghcr.io - # Version of 'regctl' to use for copying images to DOCKER_BUILD_REGISTRY - REGCTL_VERSION: 'v0.9.2' jobs: docker-build: @@ -302,8 +300,11 @@ jobs: # See https://github.com/regclient/regclient/blob/main/docs/regctl.md - name: Install regctl for Docker registry tools run: | - curl -L https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64 > /usr/bin/regctl - chmod 755 /usr/bin/regctl + export REGCTL_VERSION=v0.9.2 + mkdir -p bin + curl -sSLo bin/regctl https://github.com/regclient/regclient/releases/download/${REGCTL_VERSION}/regctl-linux-amd64 + chmod a+x bin/regctl + echo "$(pwd)/bin" >> $GITHUB_PATH # This recreates Docker tags for DockerHub - name: Add Docker metadata for image From 8e9650e0c8edc2b423ba68b2bca5f2ae657de36d Mon Sep 17 00:00:00 2001 From: Marsa Haoua Date: Thu, 13 Nov 2025 18:42:52 +0100 Subject: [PATCH 410/701] Fix 11547: SWORDv2 - read, update and delete operations (cherry picked from commit 9514ded1cd813f442a95df429487c2bfbbd9a4cf) --- .../src/main/java/org/dspace/sword2/SwordUrlManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java index eee3627c4045..1d49bc27977c 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java @@ -107,7 +107,7 @@ public String getSwordBaseUrl() "Unable to construct service document urls, due to missing/invalid " + "config in sword2.url and/or dspace.server.url"); } - sUrl = buildSWORDUrl("swordv2"); + sUrl = dspaceUrl + "/" + swordPath; } return sUrl; } From 5b2ef7e01d87dab91c226cce8ad004333c0d2bd7 Mon Sep 17 00:00:00 2001 From: Marsa Haoua Date: Thu, 13 Nov 2025 18:42:52 +0100 Subject: [PATCH 411/701] Fix 11547: SWORDv2 - read, update and delete operations (cherry picked from commit 9514ded1cd813f442a95df429487c2bfbbd9a4cf) --- .../src/main/java/org/dspace/sword2/SwordUrlManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java index eee3627c4045..1d49bc27977c 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java @@ -107,7 +107,7 @@ public String getSwordBaseUrl() "Unable to construct service document urls, due to missing/invalid " + "config in sword2.url and/or dspace.server.url"); } - sUrl = buildSWORDUrl("swordv2"); + sUrl = dspaceUrl + "/" + swordPath; } return sUrl; } From 6b0085f0b15eefb8fa0314d2ee33ca530cf8042d Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 24 Oct 2025 17:24:50 +0200 Subject: [PATCH 412/701] rename(DuplicateDetectionTest): Renames DuplicateDetectionTest to DuplicateDetectionIT fix(DuplicateDetectionTest): Deactivates security policies while creating DSpaceObjects ref: UXP-307 (cherry picked from commit 2967a2ff13e5e7f1619de93d6bbe81b7a90d38d5) --- ...ionTest.java => DuplicateDetectionIT.java} | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) rename dspace-api/src/test/java/org/dspace/content/{DuplicateDetectionTest.java => DuplicateDetectionIT.java} (97%) diff --git a/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionTest.java b/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java similarity index 97% rename from dspace-api/src/test/java/org/dspace/content/DuplicateDetectionTest.java rename to dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java index 0b6c909f03e8..4bc25fb731ac 100644 --- a/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionTest.java +++ b/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java @@ -31,7 +31,6 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; -import org.junit.Before; import org.junit.Test; /** @@ -40,7 +39,7 @@ * * @author Kim Shepherd */ -public class DuplicateDetectionTest extends AbstractIntegrationTestWithDatabase { +public class DuplicateDetectionIT extends AbstractIntegrationTestWithDatabase { private DuplicateDetectionService duplicateDetectionService = ContentServiceFactory.getInstance() .getDuplicateDetectionService(); private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @@ -56,7 +55,6 @@ public class DuplicateDetectionTest extends AbstractIntegrationTestWithDatabase private static final Logger log = LogManager.getLogger(); - @Before public void setUp() throws Exception { super.setUp(); // Temporarily enable duplicate detection and set comparison distance to 1 @@ -100,7 +98,7 @@ public void setUp() throws Exception { .withAuthor("Smith, Donald Y.") .withSubject("ExtraEntry 3") .build(); - + context.restoreAuthSystemState(); } @@ -210,7 +208,7 @@ public void testSearchDuplicates() throws Exception { public void testSearchDuplicatesWithReservedSolrCharacters() throws Exception { - + context.turnOffAuthorisationSystem(); Item item4 = ItemBuilder.createItem(context, col) .withTitle("Testing: An Important Development Step") .withIssueDate(item1IssueDate) @@ -223,6 +221,7 @@ public void testSearchDuplicatesWithReservedSolrCharacters() throws Exception { .withAuthor("Smith, Donald X.") .withSubject("ExtraEntry 2") .build(); + context.restoreAuthSystemState(); // Get potential duplicates of item 4 and make sure no exceptions are thrown List potentialDuplicates = new ArrayList<>(); @@ -254,6 +253,7 @@ public void testSearchDuplicatesWithReservedSolrCharacters() throws Exception { @Test public void testSearchDuplicatesWithVeryLongTitle() throws Exception { + context.turnOffAuthorisationSystem(); Item item6 = ItemBuilder.createItem(context, col) .withTitle("Testing: This title is over 200 characters long and should behave just the same as a " + "shorter title, with or without reserved characters. This integration test will prove that " + @@ -272,6 +272,8 @@ public void testSearchDuplicatesWithVeryLongTitle() throws Exception { .withSubject("ExtraEntry 2") .build(); + context.restoreAuthSystemState(); + // Get potential duplicates of item 4 and make sure no exceptions are thrown List potentialDuplicates = new ArrayList<>(); try { @@ -303,6 +305,8 @@ public void testSearchDuplicatesExactMatch() throws Exception { // Set distance to 0 manually configurationService.setProperty("duplicate.comparison.distance", 0); + context.turnOffAuthorisationSystem(); + Item item8 = ItemBuilder.createItem(context, col) .withTitle("This integration test will prove that the edit distance of 0 results in an exact match") .withIssueDate(item1IssueDate) @@ -323,7 +327,7 @@ public void testSearchDuplicatesExactMatch() throws Exception { .withAuthor("Smith, Donald X.") .withSubject("ExtraEntry") .build(); - + context.restoreAuthSystemState(); // Get potential duplicates of item 4 and make sure no exceptions are thrown List potentialDuplicates = new ArrayList<>(); try { @@ -387,6 +391,8 @@ public void testSearchDuplicatesWithMultipleFields() throws Exception { configurationService.setProperty("duplicate.comparison.metadata.field", new String[]{"dc.title", "dc.contributor.author"}); + context.turnOffAuthorisationSystem(); + Item item10 = ItemBuilder.createItem(context, col) .withTitle("Compare both title and author") .withIssueDate(item1IssueDate) @@ -407,6 +413,8 @@ public void testSearchDuplicatesWithMultipleFields() throws Exception { .withSubject("ExtraEntry 2") .build(); + context.restoreAuthSystemState(); + // Get potential duplicates of item 10 and make sure no exceptions are thrown List potentialDuplicates = new ArrayList<>(); try { From e39f6cddc4d888d608b54fbe5b0d2924d6ff8d3a Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 6 Nov 2025 09:19:43 +0100 Subject: [PATCH 413/701] fix(DuplicateDetectionIT): Adds the Override annotation on the setup method. ref: DURACOM-408 (cherry picked from commit bb634cf19eab82b84a47532ce301669743003761) --- .../test/java/org/dspace/content/DuplicateDetectionIT.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java b/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java index 4bc25fb731ac..424233b47898 100644 --- a/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java +++ b/dspace-api/src/test/java/org/dspace/content/DuplicateDetectionIT.java @@ -17,8 +17,6 @@ import java.util.List; import java.util.Optional; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; import org.dspace.AbstractIntegrationTestWithDatabase; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; @@ -53,8 +51,7 @@ public class DuplicateDetectionIT extends AbstractIntegrationTestWithDatabase { private final String item1Title = "Public item I"; private final String item1Author = "Smith, Donald"; - private static final Logger log = LogManager.getLogger(); - + @Override public void setUp() throws Exception { super.setUp(); // Temporarily enable duplicate detection and set comparison distance to 1 From efac0f1c971b999533d2937f536a797fac13db03 Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Wed, 28 May 2025 10:34:01 +0200 Subject: [PATCH 414/701] [DURACOM-360] Removed the initial article filter (cherry picked from commit a322c5093334343b9ff89efd2f4073dff0b45dd6) --- .../org/dspace/sort/OrderFormatTitle.java | 3 +- .../dspace/sort/OrderFormatTitleMarc21.java | 3 +- .../text/filter/InitialArticleWord.java | 172 --------- .../text/filter/MARC21InitialArticleWord.java | 329 ------------------ .../filter/StandardInitialArticleWord.java | 30 -- 5 files changed, 2 insertions(+), 535 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java delete mode 100644 dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java delete mode 100644 dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java diff --git a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java index b745f0719cb7..f6f9aaa38e50 100644 --- a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java +++ b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java @@ -9,7 +9,6 @@ import org.dspace.text.filter.DecomposeDiactritics; import org.dspace.text.filter.LowerCaseAndTrim; -import org.dspace.text.filter.StandardInitialArticleWord; import org.dspace.text.filter.StripDiacritics; import org.dspace.text.filter.TextFilter; @@ -20,7 +19,7 @@ */ public class OrderFormatTitle extends AbstractTextFilterOFD { { - filters = new TextFilter[] {new StandardInitialArticleWord(), + filters = new TextFilter[] { new DecomposeDiactritics(), new StripDiacritics(), new LowerCaseAndTrim()}; diff --git a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java index fa9ba297258a..9148ca2a988a 100644 --- a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java +++ b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java @@ -9,7 +9,6 @@ import org.dspace.text.filter.DecomposeDiactritics; import org.dspace.text.filter.LowerCaseAndTrim; -import org.dspace.text.filter.MARC21InitialArticleWord; import org.dspace.text.filter.StripDiacritics; import org.dspace.text.filter.StripLeadingNonAlphaNum; import org.dspace.text.filter.TextFilter; @@ -21,7 +20,7 @@ */ public class OrderFormatTitleMarc21 extends AbstractTextFilterOFD { { - filters = new TextFilter[] {new MARC21InitialArticleWord(), + filters = new TextFilter[] { new DecomposeDiactritics(), new StripDiacritics(), new StripLeadingNonAlphaNum(), diff --git a/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java deleted file mode 100644 index 167b201e0f7a..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java +++ /dev/null @@ -1,172 +0,0 @@ -/** - * 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.text.filter; - -/** - * Abstract class for implementing initial article word filters - * Allows you to create new classes with their own rules for mapping - * languages to article word lists. - * - * @author Graham Triggs - */ -public abstract class InitialArticleWord implements TextFilter { - /** - * When no language is passed, use null and let implementation decide what to do - */ - @Override - public String filter(String str) { - return filter(str, null); - } - - /** - * Do an initial definite/indefinite article filter on the passed string. - * On matching an initial word, can strip or move to the end, depending on the - * configuration of the implementing class. - * - * @param str The string to parse - * @param lang The language of the passed string - * @return String The filtered string - */ - @Override - public String filter(String str, String lang) { - // Get the list of article words for this language - String[] articleWordArr = getArticleWords(lang); - - // If we have an article word array, process the string - if (articleWordArr != null && articleWordArr.length > 0) { - String initialArticleWord = null; - int curPos = 0; - int initialStart = -1; - int initialEnd = -1; - - // Iterate through the characters until we find something significant, or hit the end - while (initialEnd < 0 && curPos < str.length()) { - // Have we found a significant character - if (Character.isLetterOrDigit(str.charAt(curPos))) { - // Mark this as the cut point for the initial word - initialStart = curPos; - - // Loop through the article words looking for a match - for (int idx = 0; initialEnd < 0 && idx < articleWordArr.length; idx++) { - // Extract a fragment from the string to test - // Must be same length as the article word - if (idx > 1 && initialArticleWord != null) { - // Only need to do so if we haven't already got one - // of the right length - if (initialArticleWord.length() != articleWordArr[idx].length()) { - initialArticleWord = extractText(str, curPos, articleWordArr[idx].length()); - } - } else { - initialArticleWord = extractText(str, curPos, articleWordArr[idx].length()); - } - - // Does the fragment match an article word? - if (initialArticleWord != null && initialArticleWord.equalsIgnoreCase(articleWordArr[idx])) { - // Check to see if the next character in the source - // is a whitespace - boolean isNextWhitespace = Character.isWhitespace( - str.charAt(curPos + articleWordArr[idx].length()) - ); - - // Check to see if the last character of the article word is a letter or digit - boolean endsLetterOrDigit = Character - .isLetterOrDigit(initialArticleWord.charAt(initialArticleWord.length() - 1)); - - // If the last character of the article word is a letter or digit, - // then it must be followed by whitespace, if not, it can be anything - // Setting endPos signifies that we have found an article word - if (endsLetterOrDigit && isNextWhitespace) { - initialEnd = curPos + initialArticleWord.length(); - } else if (!endsLetterOrDigit) { - initialEnd = curPos + initialArticleWord.length(); - } - } - } - - // Quit the loop, as we have a significant character - break; - } - - // Keep going - curPos++; - } - - // If endPos is positive, then we've found an article word - if (initialEnd > 0) { - // Find a cut point in the source string, removing any whitespace after the article word - int cutPos = initialEnd; - while (cutPos < str.length() && Character.isWhitespace(str.charAt(cutPos))) { - cutPos++; - } - - // Are we stripping the article word? - if (stripInitialArticle) { - // Yes, simply return everything after the cut - return str.substring(cutPos); - } else { - // No - move the initial article word to the end - return new StringBuilder(str.substring(cutPos)) - .append(wordSeparator) - .append(str.substring(initialStart, initialEnd)) - .toString(); - } - } - } - - // Didn't do any processing, or didn't find an initial article word - // Return the original string - return str; - } - - protected InitialArticleWord(boolean stripWord) { - this.wordSeparator = ", "; - stripInitialArticle = stripWord; - } - - protected InitialArticleWord() { - this.wordSeparator = ", "; - stripInitialArticle = false; - } - - /** - * Abstract method to get the list of words to use in the initial word filter - * - * @param lang The language to retrieve article words for - * @return An array of definite/indefinite article words - */ - protected abstract String[] getArticleWords(String lang); - // Separator to use when appending article to end - private final String wordSeparator; - - // Flag to signify initial article word should be removed - // If false, then the initial article word is appended to the end - private boolean stripInitialArticle = false; - - /** - * Helper method to extract text from a string. - * Ensures that there is significant data (ie. non-whitespace) - * after the segment requested. - * - * @param str - * @param pos - * @param len - * @return - */ - private String extractText(String str, int pos, int len) { - int testPos = pos + len; - while (testPos < str.length() && Character.isWhitespace(str.charAt(testPos))) { - testPos++; - } - - if (testPos < str.length()) { - return str.substring(pos, pos + len); - } - - return null; - } -} diff --git a/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java deleted file mode 100644 index c82b9ccfcf83..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java +++ /dev/null @@ -1,329 +0,0 @@ -/** - * 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.text.filter; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.dspace.services.factory.DSpaceServicesFactory; - -/** - * Implements MARC 21 standards to disregard initial - * definite or indefinite article in sorting. - * - * Note: This only works for languages defined with IANA code entries. - * - * @author Graham Triggs - */ -public class MARC21InitialArticleWord extends InitialArticleWord { - public MARC21InitialArticleWord() { - // Default behaviour is to strip the initial word completely - super(true); - } - - public MARC21InitialArticleWord(boolean stripWord) { - super(stripWord); - } - - /** - * Return the list of definite and indefinite article codes - * for this language. - */ - @Override - protected String[] getArticleWords(String lang) { - // No language - no words - if (StringUtils.isEmpty(lang)) { - return defaultWords; - } - - Language l = Language.getLanguage(lang); - - // Is the language in our map? - if (l != null && ianaArticleMap.containsKey(l.IANA)) { - // Get the list of words for this language - ArticlesForLang articles = ianaArticleMap.get(l.IANA); - - if (articles != null) { - return articles.words; - } - } - - return null; - } - - // Mapping of IANA codes to article word lists - private static Map ianaArticleMap = new HashMap(); - - private static String[] defaultWords = null; - - // Static initialisation - convert word -> languages map - // into language -> words map - static { - /* Define a mapping for article words to the languages that have them. - * Take from: http://www.loc.gov/marc/bibliographic/bdapp-e.html - */ - Object[][] articleWordArray = { - {"a", Language.ENGLISH, Language.GALICIAN, Language.HUNGARIAN, Language.PORTUGUESE, Language.ROMANIAN, - Language.SCOTS, Language.YIDDISH}, - {"a'", Language.SCOTTISH_GAELIC}, - {"al", Language.ROMANIAN}, - {"al-", Language.ARABIC, Language.BALUCHI, Language.BRAHUI, Language.PANJABI, Language.PERSIAN, - Language.TURKISH, Language.URDU}, - {"am", Language.SCOTTISH_GAELIC}, - {"an", Language.ENGLISH, Language.IRISH, Language.SCOTS, Language.SCOTTISH_GAELIC, Language.YIDDISH}, - {"an t-", Language.IRISH, Language.SCOTTISH_GAELIC}, - {"ane", Language.SCOTS}, - {"ang", Language.TAGALOG}, - {"ang mga", Language.TAGALOG}, - {"as", Language.GALICIAN, Language.PORTUGUESE}, - {"az", Language.HUNGARIAN}, - {"bat", Language.BASQUE}, - {"bir", Language.TURKISH}, - {"d'", Language.ENGLISH}, - {"da", Language.SHETLAND_ENGLISH}, - {"das", Language.GERMAN}, - {"de", Language.DANISH, Language.DUTCH, Language.ENGLISH, Language.FRISIAN, Language.NORWEGIAN, - Language.SWEDISH}, - {"dei", Language.NORWEGIAN}, - {"dem", Language.GERMAN}, - {"den", Language.DANISH, Language.GERMAN, Language.NORWEGIAN, Language.SWEDISH}, - {"der", Language.GERMAN, Language.YIDDISH}, - {"des", Language.GERMAN, Language.WALLOON}, - {"det", Language.DANISH, Language.NORWEGIAN, Language.SWEDISH}, - {"di", Language.YIDDISH}, - {"die", Language.AFRIKAANS, Language.GERMAN, Language.YIDDISH}, - {"dos", Language.YIDDISH}, - {"e", Language.NORWEGIAN}, - {"e", Language.FRISIAN}, // should be 'e - leading apostrophes are ignored - {"een", Language.DUTCH}, - {"eene", Language.DUTCH}, - {"egy", Language.HUNGARIAN}, - {"ei", Language.NORWEGIAN}, - {"ein", Language.GERMAN, Language.NORWEGIAN, Language.WALLOON}, - {"eine", Language.GERMAN}, - {"einem", Language.GERMAN}, - {"einen", Language.GERMAN}, - {"einer", Language.GERMAN}, - {"eines", Language.GERMAN}, - {"eit", Language.NORWEGIAN}, - {"el", Language.CATALAN, Language.SPANISH}, - {"el-", Language.ARABIC}, - {"els", Language.CATALAN}, - {"en", Language.CATALAN, Language.DANISH, Language.NORWEGIAN, Language.SWEDISH}, - {"enne", Language.WALLOON}, - {"et", Language.DANISH, Language.NORWEGIAN}, - {"ett", Language.SWEDISH}, - {"eyn", Language.YIDDISH}, - {"eyne", Language.YIDDISH}, - {"gl'", Language.ITALIAN}, - {"gli", Language.PROVENCAL}, - {"ha-", Language.HEBREW}, - {"hai", Language.CLASSICAL_GREEK, Language.GREEK}, - {"he", Language.HAWAIIAN}, - {"h\u0113", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"he-", Language.HEBREW}, - {"heis", Language.GREEK}, - {"hen", Language.GREEK}, - {"hena", Language.GREEK}, - {"henas", Language.GREEK}, - {"het", Language.DUTCH}, - {"hin", Language.ICELANDIC}, - {"hina", Language.ICELANDIC}, - {"hinar", Language.ICELANDIC}, - {"hinir", Language.ICELANDIC}, - {"hinn", Language.ICELANDIC}, - {"hinna", Language.ICELANDIC}, - {"hinnar", Language.ICELANDIC}, - {"hinni", Language.ICELANDIC}, - {"hins", Language.ICELANDIC}, - {"hinu", Language.ICELANDIC}, - {"hinum", Language.ICELANDIC}, - {"hi\u01d2", Language.ICELANDIC}, - {"ho", Language.CLASSICAL_GREEK, Language.GREEK}, - {"hoi", Language.CLASSICAL_GREEK, Language.GREEK}, - {"i", Language.ITALIAN}, - {"ih'", Language.PROVENCAL}, - {"il", Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"il-", Language.MALTESE}, - {"in", Language.FRISIAN}, - {"it", Language.FRISIAN}, - {"ka", Language.HAWAIIAN}, - {"ke", Language.HAWAIIAN}, - {"l'", Language.CATALAN, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.WALLOON}, - {"l-", Language.MALTESE}, - {"la", Language.CATALAN, Language.ESPERANTO, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, - Language.SPANISH}, - {"las", Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"le", Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"les", Language.CATALAN, Language.FRENCH, Language.PROVENCAL_OCCITAN, Language.WALLOON}, - {"lh", Language.PROVENCAL_OCCITAN}, - {"lhi", Language.PROVENCAL_OCCITAN}, - {"li", Language.PROVENCAL_OCCITAN}, - {"lis", Language.PROVENCAL_OCCITAN}, - {"lo", Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"los", Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"lou", Language.PROVENCAL_OCCITAN}, - {"lu", Language.PROVENCAL_OCCITAN}, - {"mga", Language.TAGALOG}, - {"m\u0303ga", Language.TAGALOG}, - {"mia", Language.GREEK}, - {"n", Language.AFRIKAANS, Language.DUTCH, Language.FRISIAN}, // should be 'n - leading - // apostrophes are ignored - {"na", Language.HAWAIIAN, Language.IRISH, Language.SCOTTISH_GAELIC}, - {"na h-", Language.IRISH, Language.SCOTTISH_GAELIC}, - {"nje", Language.ALBANIAN}, - {"ny", Language.MALAGASY}, - {"o", Language.NEAPOLITAN_ITALIAN}, // should be 'o - leading apostrophes are ignored - {"o", Language.GALICIAN, Language.HAWAIIAN, Language.PORTUGUESE, Language.ROMANIAN}, - {"os", Language.PORTUGUESE}, - {"r", Language.ICELANDIC}, // should be 'r - leading apostrophes are ignored - {"s", Language.GERMAN}, // should be 's - leading apostrophes are ignored - {"sa", Language.TAGALOG}, - {"sa mga", Language.TAGALOG}, - {"si", Language.TAGALOG}, - {"sin\u00e1", Language.TAGALOG}, - {"t", Language.DUTCH, Language.FRISIAN}, // should be 't - leading apostrophes are ignored - {"ta", Language.CLASSICAL_GREEK, Language.GREEK}, - {"tais", Language.CLASSICAL_GREEK}, - {"tas", Language.CLASSICAL_GREEK}, - {"t\u0113", Language.CLASSICAL_GREEK}, // e macron - {"t\u0113n", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"t\u0113s", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"the", Language.ENGLISH}, - {"t\u014d", Language.CLASSICAL_GREEK, Language.GREEK}, // o macron - {"tois", Language.CLASSICAL_GREEK}, - {"t\u014dn", Language.CLASSICAL_GREEK, Language.GREEK}, // o macron - {"tou", Language.CLASSICAL_GREEK, Language.GREEK}, - {"um", Language.PORTUGUESE}, - {"uma", Language.PORTUGUESE}, - {"un", Language.CATALAN, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.ROMANIAN, - Language.SPANISH}, - {"un'", Language.ITALIAN}, - {"una", Language.CATALAN, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"une", Language.FRENCH}, - {"unei", Language.ROMANIAN}, - {"unha", Language.GALICIAN}, - {"uno", Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"uns", Language.PROVENCAL_OCCITAN}, - {"unui", Language.ROMANIAN}, - {"us", Language.PROVENCAL_OCCITAN}, - {"y", Language.WELSH}, - {"ye", Language.ENGLISH}, - {"yr", Language.WELSH} - }; - - // Initialize the lang -> article map - ianaArticleMap = new HashMap(); - - int wordIdx = 0; - int langIdx = 0; - - // Iterate through word/language array - // Generate temporary language map - Map> langWordMap = new HashMap>(); - for (wordIdx = 0; wordIdx < articleWordArray.length; wordIdx++) { - for (langIdx = 1; langIdx < articleWordArray[wordIdx].length; langIdx++) { - Language lang = (Language) articleWordArray[wordIdx][langIdx]; - - if (lang != null && lang.IANA.length() > 0) { - List words = langWordMap.get(lang); - - if (words == null) { - words = new ArrayList(); - langWordMap.put(lang, words); - } - - // Add language to list if we haven't done so already - if (!words.contains(articleWordArray[wordIdx][0])) { - words.add((String) articleWordArray[wordIdx][0]); - } - } - } - } - - // Iterate through languages - for (Map.Entry> langToWord : langWordMap.entrySet()) { - Language lang = langToWord.getKey(); - List wordList = langToWord.getValue(); - - // Convert the list into an array of strings - String[] words = new String[wordList.size()]; - - for (int idx = 0; idx < wordList.size(); idx++) { - words[idx] = wordList.get(idx); - } - - // Sort the array into length order - longest to shortest - // This ensures maximal matching on the article words - Arrays.sort(words, new MARC21InitialArticleWord.InverseLengthComparator()); - - // Add language/article entry to map - ianaArticleMap.put(lang.IANA, new MARC21InitialArticleWord.ArticlesForLang(lang, words)); - } - - // Setup default stop words for null languages - String[] defaultLangs = DSpaceServicesFactory.getInstance().getConfigurationService() - .getArrayProperty("marc21wordfilter.defaultlang"); - if (ArrayUtils.isNotEmpty(defaultLangs)) { - int wordCount = 0; - ArticlesForLang[] afl = new ArticlesForLang[defaultLangs.length]; - - for (int idx = 0; idx < afl.length; idx++) { - Language l = Language.getLanguage(defaultLangs[idx]); - if (l != null && ianaArticleMap.containsKey(l.IANA)) { - afl[idx] = ianaArticleMap.get(l.IANA); - if (afl[idx] != null) { - wordCount += afl[idx].words.length; - } - } - } - - if (wordCount > 0) { - int destPos = 0; - defaultWords = new String[wordCount]; - for (int idx = 0; idx < afl.length; idx++) { - if (afl[idx] != null) { - System.arraycopy(afl[idx].words, 0, defaultWords, destPos, afl[idx].words.length); - destPos += afl[idx].words.length; - } - } - } - } - } - - // Wrapper class for inserting word arrays into a map - private static class ArticlesForLang { - final Language lang; - final String[] words; - - ArticlesForLang(Language lang, String[] words) { - this.lang = lang; - this.words = (String[]) ArrayUtils.clone(words); - } - } - - // Compare strings according to their length - longest to shortest - private static class InverseLengthComparator implements Comparator, Serializable { - @Override - public int compare(Object arg0, Object arg1) { - return ((String) arg1).length() - ((String) arg0).length(); - } - - ; - - } - - ; -} diff --git a/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java deleted file mode 100644 index ade72b150f5c..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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.text.filter; - -/** - * Implements existing DSpace initial article word behaviour - * - * Note: This only works for languages defined with ISO code entries. - * - * @author Graham Triggs - */ -public class StandardInitialArticleWord extends InitialArticleWord { - private static final String[] articleWords = {"the", "an", "a"}; - - @Override - protected String[] getArticleWords(String lang) { - if (lang != null && lang.startsWith("en")) { - return articleWords; - } - - return null; - } - -} - From d4d430ead7acd21479658383530309037302321d Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Wed, 28 May 2025 11:46:19 +0200 Subject: [PATCH 415/701] [DURACOM-360] Removed unused class (cherry picked from commit 57ef735a51ff30673fce6ca04a7674f45755ed7d) --- .../java/org/dspace/text/filter/Language.java | 142 ------------------ 1 file changed, 142 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/text/filter/Language.java diff --git a/dspace-api/src/main/java/org/dspace/text/filter/Language.java b/dspace-api/src/main/java/org/dspace/text/filter/Language.java deleted file mode 100644 index 9be68d2ddfb9..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/Language.java +++ /dev/null @@ -1,142 +0,0 @@ -/** - * 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.text.filter; - -import java.util.HashMap; -import java.util.Map; - -/** - * Define languages - both as IANA and ISO639-2 codes - * - * @author Graham Triggs - */ -public class Language { - public final String IANA; - public final String ISO639_1; - public final String ISO639_2; - - public static final Language AFRIKAANS = Language.create("af", "af", "afr"); - public static final Language ALBANIAN = Language.create("sq", "sq", "alb"); - public static final Language ARABIC = Language.create("ar", "ar", "ara"); - public static final Language BALUCHI = Language.create("bal", "", "bal"); - public static final Language BASQUE = Language.create("eu", "", "baq"); - public static final Language BRAHUI = Language.create("", "", ""); - public static final Language CATALAN = Language.create("ca", "ca", "cat"); - public static final Language CLASSICAL_GREEK = Language.create("grc", "", "grc"); - public static final Language DANISH = Language.create("da", "da", "dan"); - public static final Language DUTCH = Language.create("nl", "ni", "dut"); - public static final Language ENGLISH = Language.create("en", "en", "eng"); - public static final Language ESPERANTO = Language.create("eo", "eo", "epo"); - public static final Language FRENCH = Language.create("fr", "fr", "fre"); - public static final Language FRISIAN = Language.create("fy", "fy", "fri"); - public static final Language GALICIAN = Language.create("gl", "gl", "glg"); - public static final Language GERMAN = Language.create("de", "de", "ger"); - public static final Language GREEK = Language.create("el", "el", "gre"); - public static final Language HAWAIIAN = Language.create("haw", "", "haw"); - public static final Language HEBREW = Language.create("he", "he", "heb"); - public static final Language HUNGARIAN = Language.create("hu", "hu", "hun"); - public static final Language ICELANDIC = Language.create("is", "is", "ice"); - public static final Language IRISH = Language.create("ga", "ga", "gle"); - public static final Language ITALIAN = Language.create("it", "it", "ita"); - public static final Language MALAGASY = Language.create("mg", "mg", "mlg"); - public static final Language MALTESE = Language.create("mt", "mt", "mlt"); - public static final Language NEAPOLITAN_ITALIAN = Language.create("nap", "", "nap"); - public static final Language NORWEGIAN = Language.create("no", "no", "nor"); - public static final Language PORTUGUESE = Language.create("pt", "pt", "por"); - public static final Language PANJABI = Language.create("pa", "pa", "pan"); - public static final Language PERSIAN = Language.create("fa", "fa", "per"); - public static final Language PROVENCAL = Language.create("pro", "", "pro"); - public static final Language PROVENCAL_OCCITAN = Language.create("oc", "oc", "oci"); - public static final Language ROMANIAN = Language.create("ro", "ro", "rum"); - public static final Language SCOTS = Language.create("sco", "", "sco"); - public static final Language SCOTTISH_GAELIC = Language.create("gd", "gd", "gae"); - public static final Language SHETLAND_ENGLISH = Language.create("", "", ""); - public static final Language SPANISH = Language.create("es", "es", "spa"); - public static final Language SWEDISH = Language.create("sv", "sv", "swe"); - public static final Language TAGALOG = Language.create("tl", "tl", "tgl"); - public static final Language TURKISH = Language.create("tr", "tr", "tur"); - public static final Language URDU = Language.create("ur", "ur", "urd"); - public static final Language WALLOON = Language.create("wa", "wa", "wln"); - public static final Language WELSH = Language.create("cy", "cy", "wel"); - public static final Language YIDDISH = Language.create("yi", "yi", "yid"); - - public static Language getLanguage(String lang) { - return LanguageMaps.getLanguage(lang); - } - - public static Language getLanguageForIANA(String iana) { - return LanguageMaps.getLanguageForIANA(iana); - } - - public static Language getLanguageForISO639_2(String iso) { - return LanguageMaps.getLanguageForISO639_2(iso); - } - - private static synchronized Language create(String iana, String iso639_1, String iso639_2) { - Language lang = LanguageMaps.getLanguageForIANA(iana); - - lang = (lang != null ? lang : LanguageMaps.getLanguageForISO639_1(iso639_1)); - lang = (lang != null ? lang : LanguageMaps.getLanguageForISO639_2(iso639_2)); - - return (lang != null ? lang : new Language(iana, iso639_1, iso639_2)); - } - - private static class LanguageMaps { - private static final Map langMapIANA = new HashMap(); - private static final Map langMapISO639_1 = new HashMap(); - private static final Map langMapISO639_2 = new HashMap(); - - static void add(Language l) { - if (l.IANA != null && l.IANA.length() > 0 && !langMapIANA.containsKey(l.IANA)) { - langMapIANA.put(l.IANA, l); - } - - if (l.ISO639_1 != null && l.ISO639_1.length() > 0 && !langMapISO639_1.containsKey(l.ISO639_1)) { - langMapISO639_1.put(l.ISO639_1, l); - } - - if (l.ISO639_2 != null && l.ISO639_2.length() > 0 && !langMapISO639_2.containsKey(l.ISO639_2)) { - langMapISO639_2.put(l.ISO639_2, l); - } - } - - public static Language getLanguage(String lang) { - if (langMapIANA.containsKey(lang)) { - return langMapIANA.get(lang); - } - - return langMapISO639_2.get(lang); - } - - public static Language getLanguageForIANA(String iana) { - return langMapIANA.get(iana); - } - - public static Language getLanguageForISO639_1(String iso) { - return langMapISO639_1.get(iso); - } - - public static Language getLanguageForISO639_2(String iso) { - return langMapISO639_2.get(iso); - } - } - - private Language(String iana, String iso639_1, String iso639_2) { - IANA = iana; - ISO639_1 = iso639_1; - ISO639_2 = iso639_2; - - LanguageMaps.add(this); - } - - private Language() { - IANA = null; - ISO639_1 = null; - ISO639_2 = null; - } -} From 93405e0946e198a8114aad0dfb323699022f0af3 Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Wed, 28 May 2025 10:34:01 +0200 Subject: [PATCH 416/701] [DURACOM-360] Removed the initial article filter (cherry picked from commit a322c5093334343b9ff89efd2f4073dff0b45dd6) --- .../org/dspace/sort/OrderFormatTitle.java | 3 +- .../dspace/sort/OrderFormatTitleMarc21.java | 3 +- .../text/filter/InitialArticleWord.java | 172 --------- .../text/filter/MARC21InitialArticleWord.java | 329 ------------------ .../filter/StandardInitialArticleWord.java | 30 -- 5 files changed, 2 insertions(+), 535 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java delete mode 100644 dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java delete mode 100644 dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java diff --git a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java index b745f0719cb7..f6f9aaa38e50 100644 --- a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java +++ b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitle.java @@ -9,7 +9,6 @@ import org.dspace.text.filter.DecomposeDiactritics; import org.dspace.text.filter.LowerCaseAndTrim; -import org.dspace.text.filter.StandardInitialArticleWord; import org.dspace.text.filter.StripDiacritics; import org.dspace.text.filter.TextFilter; @@ -20,7 +19,7 @@ */ public class OrderFormatTitle extends AbstractTextFilterOFD { { - filters = new TextFilter[] {new StandardInitialArticleWord(), + filters = new TextFilter[] { new DecomposeDiactritics(), new StripDiacritics(), new LowerCaseAndTrim()}; diff --git a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java index fa9ba297258a..9148ca2a988a 100644 --- a/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java +++ b/dspace-api/src/main/java/org/dspace/sort/OrderFormatTitleMarc21.java @@ -9,7 +9,6 @@ import org.dspace.text.filter.DecomposeDiactritics; import org.dspace.text.filter.LowerCaseAndTrim; -import org.dspace.text.filter.MARC21InitialArticleWord; import org.dspace.text.filter.StripDiacritics; import org.dspace.text.filter.StripLeadingNonAlphaNum; import org.dspace.text.filter.TextFilter; @@ -21,7 +20,7 @@ */ public class OrderFormatTitleMarc21 extends AbstractTextFilterOFD { { - filters = new TextFilter[] {new MARC21InitialArticleWord(), + filters = new TextFilter[] { new DecomposeDiactritics(), new StripDiacritics(), new StripLeadingNonAlphaNum(), diff --git a/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java deleted file mode 100644 index 167b201e0f7a..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/InitialArticleWord.java +++ /dev/null @@ -1,172 +0,0 @@ -/** - * 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.text.filter; - -/** - * Abstract class for implementing initial article word filters - * Allows you to create new classes with their own rules for mapping - * languages to article word lists. - * - * @author Graham Triggs - */ -public abstract class InitialArticleWord implements TextFilter { - /** - * When no language is passed, use null and let implementation decide what to do - */ - @Override - public String filter(String str) { - return filter(str, null); - } - - /** - * Do an initial definite/indefinite article filter on the passed string. - * On matching an initial word, can strip or move to the end, depending on the - * configuration of the implementing class. - * - * @param str The string to parse - * @param lang The language of the passed string - * @return String The filtered string - */ - @Override - public String filter(String str, String lang) { - // Get the list of article words for this language - String[] articleWordArr = getArticleWords(lang); - - // If we have an article word array, process the string - if (articleWordArr != null && articleWordArr.length > 0) { - String initialArticleWord = null; - int curPos = 0; - int initialStart = -1; - int initialEnd = -1; - - // Iterate through the characters until we find something significant, or hit the end - while (initialEnd < 0 && curPos < str.length()) { - // Have we found a significant character - if (Character.isLetterOrDigit(str.charAt(curPos))) { - // Mark this as the cut point for the initial word - initialStart = curPos; - - // Loop through the article words looking for a match - for (int idx = 0; initialEnd < 0 && idx < articleWordArr.length; idx++) { - // Extract a fragment from the string to test - // Must be same length as the article word - if (idx > 1 && initialArticleWord != null) { - // Only need to do so if we haven't already got one - // of the right length - if (initialArticleWord.length() != articleWordArr[idx].length()) { - initialArticleWord = extractText(str, curPos, articleWordArr[idx].length()); - } - } else { - initialArticleWord = extractText(str, curPos, articleWordArr[idx].length()); - } - - // Does the fragment match an article word? - if (initialArticleWord != null && initialArticleWord.equalsIgnoreCase(articleWordArr[idx])) { - // Check to see if the next character in the source - // is a whitespace - boolean isNextWhitespace = Character.isWhitespace( - str.charAt(curPos + articleWordArr[idx].length()) - ); - - // Check to see if the last character of the article word is a letter or digit - boolean endsLetterOrDigit = Character - .isLetterOrDigit(initialArticleWord.charAt(initialArticleWord.length() - 1)); - - // If the last character of the article word is a letter or digit, - // then it must be followed by whitespace, if not, it can be anything - // Setting endPos signifies that we have found an article word - if (endsLetterOrDigit && isNextWhitespace) { - initialEnd = curPos + initialArticleWord.length(); - } else if (!endsLetterOrDigit) { - initialEnd = curPos + initialArticleWord.length(); - } - } - } - - // Quit the loop, as we have a significant character - break; - } - - // Keep going - curPos++; - } - - // If endPos is positive, then we've found an article word - if (initialEnd > 0) { - // Find a cut point in the source string, removing any whitespace after the article word - int cutPos = initialEnd; - while (cutPos < str.length() && Character.isWhitespace(str.charAt(cutPos))) { - cutPos++; - } - - // Are we stripping the article word? - if (stripInitialArticle) { - // Yes, simply return everything after the cut - return str.substring(cutPos); - } else { - // No - move the initial article word to the end - return new StringBuilder(str.substring(cutPos)) - .append(wordSeparator) - .append(str.substring(initialStart, initialEnd)) - .toString(); - } - } - } - - // Didn't do any processing, or didn't find an initial article word - // Return the original string - return str; - } - - protected InitialArticleWord(boolean stripWord) { - this.wordSeparator = ", "; - stripInitialArticle = stripWord; - } - - protected InitialArticleWord() { - this.wordSeparator = ", "; - stripInitialArticle = false; - } - - /** - * Abstract method to get the list of words to use in the initial word filter - * - * @param lang The language to retrieve article words for - * @return An array of definite/indefinite article words - */ - protected abstract String[] getArticleWords(String lang); - // Separator to use when appending article to end - private final String wordSeparator; - - // Flag to signify initial article word should be removed - // If false, then the initial article word is appended to the end - private boolean stripInitialArticle = false; - - /** - * Helper method to extract text from a string. - * Ensures that there is significant data (ie. non-whitespace) - * after the segment requested. - * - * @param str - * @param pos - * @param len - * @return - */ - private String extractText(String str, int pos, int len) { - int testPos = pos + len; - while (testPos < str.length() && Character.isWhitespace(str.charAt(testPos))) { - testPos++; - } - - if (testPos < str.length()) { - return str.substring(pos, pos + len); - } - - return null; - } -} diff --git a/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java deleted file mode 100644 index c82b9ccfcf83..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/MARC21InitialArticleWord.java +++ /dev/null @@ -1,329 +0,0 @@ -/** - * 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.text.filter; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.dspace.services.factory.DSpaceServicesFactory; - -/** - * Implements MARC 21 standards to disregard initial - * definite or indefinite article in sorting. - * - * Note: This only works for languages defined with IANA code entries. - * - * @author Graham Triggs - */ -public class MARC21InitialArticleWord extends InitialArticleWord { - public MARC21InitialArticleWord() { - // Default behaviour is to strip the initial word completely - super(true); - } - - public MARC21InitialArticleWord(boolean stripWord) { - super(stripWord); - } - - /** - * Return the list of definite and indefinite article codes - * for this language. - */ - @Override - protected String[] getArticleWords(String lang) { - // No language - no words - if (StringUtils.isEmpty(lang)) { - return defaultWords; - } - - Language l = Language.getLanguage(lang); - - // Is the language in our map? - if (l != null && ianaArticleMap.containsKey(l.IANA)) { - // Get the list of words for this language - ArticlesForLang articles = ianaArticleMap.get(l.IANA); - - if (articles != null) { - return articles.words; - } - } - - return null; - } - - // Mapping of IANA codes to article word lists - private static Map ianaArticleMap = new HashMap(); - - private static String[] defaultWords = null; - - // Static initialisation - convert word -> languages map - // into language -> words map - static { - /* Define a mapping for article words to the languages that have them. - * Take from: http://www.loc.gov/marc/bibliographic/bdapp-e.html - */ - Object[][] articleWordArray = { - {"a", Language.ENGLISH, Language.GALICIAN, Language.HUNGARIAN, Language.PORTUGUESE, Language.ROMANIAN, - Language.SCOTS, Language.YIDDISH}, - {"a'", Language.SCOTTISH_GAELIC}, - {"al", Language.ROMANIAN}, - {"al-", Language.ARABIC, Language.BALUCHI, Language.BRAHUI, Language.PANJABI, Language.PERSIAN, - Language.TURKISH, Language.URDU}, - {"am", Language.SCOTTISH_GAELIC}, - {"an", Language.ENGLISH, Language.IRISH, Language.SCOTS, Language.SCOTTISH_GAELIC, Language.YIDDISH}, - {"an t-", Language.IRISH, Language.SCOTTISH_GAELIC}, - {"ane", Language.SCOTS}, - {"ang", Language.TAGALOG}, - {"ang mga", Language.TAGALOG}, - {"as", Language.GALICIAN, Language.PORTUGUESE}, - {"az", Language.HUNGARIAN}, - {"bat", Language.BASQUE}, - {"bir", Language.TURKISH}, - {"d'", Language.ENGLISH}, - {"da", Language.SHETLAND_ENGLISH}, - {"das", Language.GERMAN}, - {"de", Language.DANISH, Language.DUTCH, Language.ENGLISH, Language.FRISIAN, Language.NORWEGIAN, - Language.SWEDISH}, - {"dei", Language.NORWEGIAN}, - {"dem", Language.GERMAN}, - {"den", Language.DANISH, Language.GERMAN, Language.NORWEGIAN, Language.SWEDISH}, - {"der", Language.GERMAN, Language.YIDDISH}, - {"des", Language.GERMAN, Language.WALLOON}, - {"det", Language.DANISH, Language.NORWEGIAN, Language.SWEDISH}, - {"di", Language.YIDDISH}, - {"die", Language.AFRIKAANS, Language.GERMAN, Language.YIDDISH}, - {"dos", Language.YIDDISH}, - {"e", Language.NORWEGIAN}, - {"e", Language.FRISIAN}, // should be 'e - leading apostrophes are ignored - {"een", Language.DUTCH}, - {"eene", Language.DUTCH}, - {"egy", Language.HUNGARIAN}, - {"ei", Language.NORWEGIAN}, - {"ein", Language.GERMAN, Language.NORWEGIAN, Language.WALLOON}, - {"eine", Language.GERMAN}, - {"einem", Language.GERMAN}, - {"einen", Language.GERMAN}, - {"einer", Language.GERMAN}, - {"eines", Language.GERMAN}, - {"eit", Language.NORWEGIAN}, - {"el", Language.CATALAN, Language.SPANISH}, - {"el-", Language.ARABIC}, - {"els", Language.CATALAN}, - {"en", Language.CATALAN, Language.DANISH, Language.NORWEGIAN, Language.SWEDISH}, - {"enne", Language.WALLOON}, - {"et", Language.DANISH, Language.NORWEGIAN}, - {"ett", Language.SWEDISH}, - {"eyn", Language.YIDDISH}, - {"eyne", Language.YIDDISH}, - {"gl'", Language.ITALIAN}, - {"gli", Language.PROVENCAL}, - {"ha-", Language.HEBREW}, - {"hai", Language.CLASSICAL_GREEK, Language.GREEK}, - {"he", Language.HAWAIIAN}, - {"h\u0113", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"he-", Language.HEBREW}, - {"heis", Language.GREEK}, - {"hen", Language.GREEK}, - {"hena", Language.GREEK}, - {"henas", Language.GREEK}, - {"het", Language.DUTCH}, - {"hin", Language.ICELANDIC}, - {"hina", Language.ICELANDIC}, - {"hinar", Language.ICELANDIC}, - {"hinir", Language.ICELANDIC}, - {"hinn", Language.ICELANDIC}, - {"hinna", Language.ICELANDIC}, - {"hinnar", Language.ICELANDIC}, - {"hinni", Language.ICELANDIC}, - {"hins", Language.ICELANDIC}, - {"hinu", Language.ICELANDIC}, - {"hinum", Language.ICELANDIC}, - {"hi\u01d2", Language.ICELANDIC}, - {"ho", Language.CLASSICAL_GREEK, Language.GREEK}, - {"hoi", Language.CLASSICAL_GREEK, Language.GREEK}, - {"i", Language.ITALIAN}, - {"ih'", Language.PROVENCAL}, - {"il", Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"il-", Language.MALTESE}, - {"in", Language.FRISIAN}, - {"it", Language.FRISIAN}, - {"ka", Language.HAWAIIAN}, - {"ke", Language.HAWAIIAN}, - {"l'", Language.CATALAN, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.WALLOON}, - {"l-", Language.MALTESE}, - {"la", Language.CATALAN, Language.ESPERANTO, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, - Language.SPANISH}, - {"las", Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"le", Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"les", Language.CATALAN, Language.FRENCH, Language.PROVENCAL_OCCITAN, Language.WALLOON}, - {"lh", Language.PROVENCAL_OCCITAN}, - {"lhi", Language.PROVENCAL_OCCITAN}, - {"li", Language.PROVENCAL_OCCITAN}, - {"lis", Language.PROVENCAL_OCCITAN}, - {"lo", Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"los", Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"lou", Language.PROVENCAL_OCCITAN}, - {"lu", Language.PROVENCAL_OCCITAN}, - {"mga", Language.TAGALOG}, - {"m\u0303ga", Language.TAGALOG}, - {"mia", Language.GREEK}, - {"n", Language.AFRIKAANS, Language.DUTCH, Language.FRISIAN}, // should be 'n - leading - // apostrophes are ignored - {"na", Language.HAWAIIAN, Language.IRISH, Language.SCOTTISH_GAELIC}, - {"na h-", Language.IRISH, Language.SCOTTISH_GAELIC}, - {"nje", Language.ALBANIAN}, - {"ny", Language.MALAGASY}, - {"o", Language.NEAPOLITAN_ITALIAN}, // should be 'o - leading apostrophes are ignored - {"o", Language.GALICIAN, Language.HAWAIIAN, Language.PORTUGUESE, Language.ROMANIAN}, - {"os", Language.PORTUGUESE}, - {"r", Language.ICELANDIC}, // should be 'r - leading apostrophes are ignored - {"s", Language.GERMAN}, // should be 's - leading apostrophes are ignored - {"sa", Language.TAGALOG}, - {"sa mga", Language.TAGALOG}, - {"si", Language.TAGALOG}, - {"sin\u00e1", Language.TAGALOG}, - {"t", Language.DUTCH, Language.FRISIAN}, // should be 't - leading apostrophes are ignored - {"ta", Language.CLASSICAL_GREEK, Language.GREEK}, - {"tais", Language.CLASSICAL_GREEK}, - {"tas", Language.CLASSICAL_GREEK}, - {"t\u0113", Language.CLASSICAL_GREEK}, // e macron - {"t\u0113n", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"t\u0113s", Language.CLASSICAL_GREEK, Language.GREEK}, // e macron - {"the", Language.ENGLISH}, - {"t\u014d", Language.CLASSICAL_GREEK, Language.GREEK}, // o macron - {"tois", Language.CLASSICAL_GREEK}, - {"t\u014dn", Language.CLASSICAL_GREEK, Language.GREEK}, // o macron - {"tou", Language.CLASSICAL_GREEK, Language.GREEK}, - {"um", Language.PORTUGUESE}, - {"uma", Language.PORTUGUESE}, - {"un", Language.CATALAN, Language.FRENCH, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.ROMANIAN, - Language.SPANISH}, - {"un'", Language.ITALIAN}, - {"una", Language.CATALAN, Language.ITALIAN, Language.PROVENCAL_OCCITAN, Language.SPANISH}, - {"une", Language.FRENCH}, - {"unei", Language.ROMANIAN}, - {"unha", Language.GALICIAN}, - {"uno", Language.ITALIAN, Language.PROVENCAL_OCCITAN}, - {"uns", Language.PROVENCAL_OCCITAN}, - {"unui", Language.ROMANIAN}, - {"us", Language.PROVENCAL_OCCITAN}, - {"y", Language.WELSH}, - {"ye", Language.ENGLISH}, - {"yr", Language.WELSH} - }; - - // Initialize the lang -> article map - ianaArticleMap = new HashMap(); - - int wordIdx = 0; - int langIdx = 0; - - // Iterate through word/language array - // Generate temporary language map - Map> langWordMap = new HashMap>(); - for (wordIdx = 0; wordIdx < articleWordArray.length; wordIdx++) { - for (langIdx = 1; langIdx < articleWordArray[wordIdx].length; langIdx++) { - Language lang = (Language) articleWordArray[wordIdx][langIdx]; - - if (lang != null && lang.IANA.length() > 0) { - List words = langWordMap.get(lang); - - if (words == null) { - words = new ArrayList(); - langWordMap.put(lang, words); - } - - // Add language to list if we haven't done so already - if (!words.contains(articleWordArray[wordIdx][0])) { - words.add((String) articleWordArray[wordIdx][0]); - } - } - } - } - - // Iterate through languages - for (Map.Entry> langToWord : langWordMap.entrySet()) { - Language lang = langToWord.getKey(); - List wordList = langToWord.getValue(); - - // Convert the list into an array of strings - String[] words = new String[wordList.size()]; - - for (int idx = 0; idx < wordList.size(); idx++) { - words[idx] = wordList.get(idx); - } - - // Sort the array into length order - longest to shortest - // This ensures maximal matching on the article words - Arrays.sort(words, new MARC21InitialArticleWord.InverseLengthComparator()); - - // Add language/article entry to map - ianaArticleMap.put(lang.IANA, new MARC21InitialArticleWord.ArticlesForLang(lang, words)); - } - - // Setup default stop words for null languages - String[] defaultLangs = DSpaceServicesFactory.getInstance().getConfigurationService() - .getArrayProperty("marc21wordfilter.defaultlang"); - if (ArrayUtils.isNotEmpty(defaultLangs)) { - int wordCount = 0; - ArticlesForLang[] afl = new ArticlesForLang[defaultLangs.length]; - - for (int idx = 0; idx < afl.length; idx++) { - Language l = Language.getLanguage(defaultLangs[idx]); - if (l != null && ianaArticleMap.containsKey(l.IANA)) { - afl[idx] = ianaArticleMap.get(l.IANA); - if (afl[idx] != null) { - wordCount += afl[idx].words.length; - } - } - } - - if (wordCount > 0) { - int destPos = 0; - defaultWords = new String[wordCount]; - for (int idx = 0; idx < afl.length; idx++) { - if (afl[idx] != null) { - System.arraycopy(afl[idx].words, 0, defaultWords, destPos, afl[idx].words.length); - destPos += afl[idx].words.length; - } - } - } - } - } - - // Wrapper class for inserting word arrays into a map - private static class ArticlesForLang { - final Language lang; - final String[] words; - - ArticlesForLang(Language lang, String[] words) { - this.lang = lang; - this.words = (String[]) ArrayUtils.clone(words); - } - } - - // Compare strings according to their length - longest to shortest - private static class InverseLengthComparator implements Comparator, Serializable { - @Override - public int compare(Object arg0, Object arg1) { - return ((String) arg1).length() - ((String) arg0).length(); - } - - ; - - } - - ; -} diff --git a/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java b/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java deleted file mode 100644 index ade72b150f5c..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/StandardInitialArticleWord.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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.text.filter; - -/** - * Implements existing DSpace initial article word behaviour - * - * Note: This only works for languages defined with ISO code entries. - * - * @author Graham Triggs - */ -public class StandardInitialArticleWord extends InitialArticleWord { - private static final String[] articleWords = {"the", "an", "a"}; - - @Override - protected String[] getArticleWords(String lang) { - if (lang != null && lang.startsWith("en")) { - return articleWords; - } - - return null; - } - -} - From 4c2142076c83ec1a17326a176b1f7cd86d5ae1d3 Mon Sep 17 00:00:00 2001 From: Elios Buzo Date: Wed, 28 May 2025 11:46:19 +0200 Subject: [PATCH 417/701] [DURACOM-360] Removed unused class (cherry picked from commit 57ef735a51ff30673fce6ca04a7674f45755ed7d) --- .../java/org/dspace/text/filter/Language.java | 142 ------------------ 1 file changed, 142 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/text/filter/Language.java diff --git a/dspace-api/src/main/java/org/dspace/text/filter/Language.java b/dspace-api/src/main/java/org/dspace/text/filter/Language.java deleted file mode 100644 index 9be68d2ddfb9..000000000000 --- a/dspace-api/src/main/java/org/dspace/text/filter/Language.java +++ /dev/null @@ -1,142 +0,0 @@ -/** - * 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.text.filter; - -import java.util.HashMap; -import java.util.Map; - -/** - * Define languages - both as IANA and ISO639-2 codes - * - * @author Graham Triggs - */ -public class Language { - public final String IANA; - public final String ISO639_1; - public final String ISO639_2; - - public static final Language AFRIKAANS = Language.create("af", "af", "afr"); - public static final Language ALBANIAN = Language.create("sq", "sq", "alb"); - public static final Language ARABIC = Language.create("ar", "ar", "ara"); - public static final Language BALUCHI = Language.create("bal", "", "bal"); - public static final Language BASQUE = Language.create("eu", "", "baq"); - public static final Language BRAHUI = Language.create("", "", ""); - public static final Language CATALAN = Language.create("ca", "ca", "cat"); - public static final Language CLASSICAL_GREEK = Language.create("grc", "", "grc"); - public static final Language DANISH = Language.create("da", "da", "dan"); - public static final Language DUTCH = Language.create("nl", "ni", "dut"); - public static final Language ENGLISH = Language.create("en", "en", "eng"); - public static final Language ESPERANTO = Language.create("eo", "eo", "epo"); - public static final Language FRENCH = Language.create("fr", "fr", "fre"); - public static final Language FRISIAN = Language.create("fy", "fy", "fri"); - public static final Language GALICIAN = Language.create("gl", "gl", "glg"); - public static final Language GERMAN = Language.create("de", "de", "ger"); - public static final Language GREEK = Language.create("el", "el", "gre"); - public static final Language HAWAIIAN = Language.create("haw", "", "haw"); - public static final Language HEBREW = Language.create("he", "he", "heb"); - public static final Language HUNGARIAN = Language.create("hu", "hu", "hun"); - public static final Language ICELANDIC = Language.create("is", "is", "ice"); - public static final Language IRISH = Language.create("ga", "ga", "gle"); - public static final Language ITALIAN = Language.create("it", "it", "ita"); - public static final Language MALAGASY = Language.create("mg", "mg", "mlg"); - public static final Language MALTESE = Language.create("mt", "mt", "mlt"); - public static final Language NEAPOLITAN_ITALIAN = Language.create("nap", "", "nap"); - public static final Language NORWEGIAN = Language.create("no", "no", "nor"); - public static final Language PORTUGUESE = Language.create("pt", "pt", "por"); - public static final Language PANJABI = Language.create("pa", "pa", "pan"); - public static final Language PERSIAN = Language.create("fa", "fa", "per"); - public static final Language PROVENCAL = Language.create("pro", "", "pro"); - public static final Language PROVENCAL_OCCITAN = Language.create("oc", "oc", "oci"); - public static final Language ROMANIAN = Language.create("ro", "ro", "rum"); - public static final Language SCOTS = Language.create("sco", "", "sco"); - public static final Language SCOTTISH_GAELIC = Language.create("gd", "gd", "gae"); - public static final Language SHETLAND_ENGLISH = Language.create("", "", ""); - public static final Language SPANISH = Language.create("es", "es", "spa"); - public static final Language SWEDISH = Language.create("sv", "sv", "swe"); - public static final Language TAGALOG = Language.create("tl", "tl", "tgl"); - public static final Language TURKISH = Language.create("tr", "tr", "tur"); - public static final Language URDU = Language.create("ur", "ur", "urd"); - public static final Language WALLOON = Language.create("wa", "wa", "wln"); - public static final Language WELSH = Language.create("cy", "cy", "wel"); - public static final Language YIDDISH = Language.create("yi", "yi", "yid"); - - public static Language getLanguage(String lang) { - return LanguageMaps.getLanguage(lang); - } - - public static Language getLanguageForIANA(String iana) { - return LanguageMaps.getLanguageForIANA(iana); - } - - public static Language getLanguageForISO639_2(String iso) { - return LanguageMaps.getLanguageForISO639_2(iso); - } - - private static synchronized Language create(String iana, String iso639_1, String iso639_2) { - Language lang = LanguageMaps.getLanguageForIANA(iana); - - lang = (lang != null ? lang : LanguageMaps.getLanguageForISO639_1(iso639_1)); - lang = (lang != null ? lang : LanguageMaps.getLanguageForISO639_2(iso639_2)); - - return (lang != null ? lang : new Language(iana, iso639_1, iso639_2)); - } - - private static class LanguageMaps { - private static final Map langMapIANA = new HashMap(); - private static final Map langMapISO639_1 = new HashMap(); - private static final Map langMapISO639_2 = new HashMap(); - - static void add(Language l) { - if (l.IANA != null && l.IANA.length() > 0 && !langMapIANA.containsKey(l.IANA)) { - langMapIANA.put(l.IANA, l); - } - - if (l.ISO639_1 != null && l.ISO639_1.length() > 0 && !langMapISO639_1.containsKey(l.ISO639_1)) { - langMapISO639_1.put(l.ISO639_1, l); - } - - if (l.ISO639_2 != null && l.ISO639_2.length() > 0 && !langMapISO639_2.containsKey(l.ISO639_2)) { - langMapISO639_2.put(l.ISO639_2, l); - } - } - - public static Language getLanguage(String lang) { - if (langMapIANA.containsKey(lang)) { - return langMapIANA.get(lang); - } - - return langMapISO639_2.get(lang); - } - - public static Language getLanguageForIANA(String iana) { - return langMapIANA.get(iana); - } - - public static Language getLanguageForISO639_1(String iso) { - return langMapISO639_1.get(iso); - } - - public static Language getLanguageForISO639_2(String iso) { - return langMapISO639_2.get(iso); - } - } - - private Language(String iana, String iso639_1, String iso639_2) { - IANA = iana; - ISO639_1 = iso639_1; - ISO639_2 = iso639_2; - - LanguageMaps.add(this); - } - - private Language() { - IANA = null; - ISO639_1 = null; - ISO639_2 = null; - } -} From d791eb6b720a9ddce0c843a3d75f1455c0688bd5 Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Sat, 15 Feb 2025 11:56:57 +0200 Subject: [PATCH 418/701] Clarify command line options help and messages in checker and checker-emailer Based on discussion in #10008 and #8999 with arvoConsultores's original Messages patch. Adds error message if mutually exclusive checker options are attempted to be used in combination. Adds note to log on sent report emails and unifies email report formatting. (cherry picked from commit c5000903617ed10221297df9b00abfbced9b58a5) --- .../dspace/app/checker/ChecksumChecker.java | 53 +++++++++++++------ .../dspace/checker/DailyReportEmailer.java | 41 +++++++------- .../checker/SimpleReporterServiceImpl.java | 2 +- .../src/main/resources/Messages.properties | 10 ++-- 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java b/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java index ec024c345263..160d23e32204 100644 --- a/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java +++ b/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java @@ -98,7 +98,7 @@ public static void main(String[] args) throws SQLException { options.addOption("h", "help", false, "Help"); options.addOption("d", "duration", true, "Checking duration"); options.addOption("c", "count", true, "Check count"); - options.addOption("a", "handle", true, "Specify a handle to check"); + options.addOption("i", "handle", true, "Specify a handle to check"); options.addOption("v", "verbose", false, "Report all processing"); Option option; @@ -106,7 +106,7 @@ public static void main(String[] args) throws SQLException { option = Option.builder("b") .longOpt("bitstream-ids") .hasArgs() - .desc("Space separated list of bitstream ids") + .desc("Space separated list of bitstream UUIDs") .build(); options.addOption(option); @@ -132,6 +132,17 @@ public static void main(String[] args) throws SQLException { try { context = new Context(); + int mutuallyExclusiveOpts = 0; + for (char c : new char[]{'l', 'L', 'd', 'b', 'i','c'}) { + if (line.hasOption(c)) { + mutuallyExclusiveOpts++; + } + } + if (mutuallyExclusiveOpts > 1) { + System.err.println("Please use only one option of -l, -L, -d, -b, -i, or -c"); + LOG.error("Please use only one option of -l, -L, -d, -b, -i, or -c"); + System.exit(1); + } // Prune stage if (line.hasOption('p')) { @@ -169,13 +180,13 @@ public static void main(String[] args) throws SQLException { bitstreams.add(bitstreamService.find(context, UUID.fromString(ids[i]))); } catch (NumberFormatException nfe) { System.err.println("The following argument: " + ids[i] - + " is not an integer"); + + " is not an UUID"); System.exit(0); } } dispatcher = new IteratorDispatcher(bitstreams.iterator()); - } else if (line.hasOption('a')) { - dispatcher = new HandleDispatcher(context, line.getOptionValue('a')); + } else if (line.hasOption('i')) { + dispatcher = new HandleDispatcher(context, line.getOptionValue('i')); } else if (line.hasOption('d')) { // run checker process for specified duration try { @@ -185,6 +196,8 @@ public static void main(String[] args) throws SQLException { + Utils.parseDuration(line .getOptionValue('d')))); } catch (Exception e) { + System.err.println("Couldn't parse " + line.getOptionValue('d') + + " as a duration"); LOG.fatal("Couldn't parse " + line.getOptionValue('d') + " as a duration: ", e); System.exit(0); @@ -228,18 +241,24 @@ public static void main(String[] args) throws SQLException { private static void printHelp(Options options) { HelpFormatter myhelp = new HelpFormatter(); - myhelp.printHelp("Checksum Checker\n", options); - System.out.println("\nSpecify a duration for checker process, using s(seconds)," - + "m(minutes), or h(hours): ChecksumChecker -d 30s" - + " OR ChecksumChecker -d 30m" - + " OR ChecksumChecker -d 2h"); - System.out.println("\nSpecify bitstream IDs: ChecksumChecker -b 13 15 17 20"); - System.out.println("\nLoop once through all bitstreams: " - + "ChecksumChecker -l"); - System.out.println("\nLoop continuously through all bitstreams: ChecksumChecker -L"); - System.out.println("\nCheck a defined number of bitstreams: ChecksumChecker -c 10"); - System.out.println("\nReport all processing (verbose)(default reports only errors): ChecksumChecker -v"); - System.out.println("\nDefault (no arguments) is equivalent to '-c 1'"); + myhelp.printHelp("checker\n", options); + System.out.println("\nChecksum Checker usage examples:"); + System.out.println("\nThe following options are mutually exclusive:"); + System.out.println(" - Specify a duration for checker process, using s(seconds)," + + "m(minutes), or h(hours): checker -d 30s" + + " OR checker -d 30m" + + " OR checker -d 2h"); + System.out.println(" - Specify bitstream UUIDs: checker -b 550e8400-e29b-41d4-a716-446655440000" + + " f3f2e850-b5d4-11ef-ac7e-96584d5248b2"); + System.out.println(" - Specify handle: checker -i 12345/100"); + System.out.println(" - Loop once through all bitstreams: " + + "checker -l"); + System.out.println(" - Loop continuously through all bitstreams: checker -L"); + System.out.println(" - Check a defined number of bitstreams: checker -c 10"); + System.out.println("\nThe following options can be used in combination with others above:"); + System.out.println(" - Report all processing to checker.log (by default logs only errors): checker -v"); + System.out.println(" - Prune old results from the database: checker -p"); + System.out.println("\nDefault (no arguments) is equivalent to 'checker -c 1'\n"); System.exit(0); } diff --git a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java index b291232e8b55..f32ddc232385 100644 --- a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java +++ b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java @@ -75,6 +75,7 @@ public void sendReport(File attachment, int numberOfBitstreams) email.setContent("Checker Report", "report is attached ..."); email.addAttachment(attachment, "checksum_checker_report.txt"); email.addRecipient(configurationService.getProperty("mail.admin")); + log.info("Sending checker report email to " + configurationService.getProperty("mail.admin")); email.send(); } } @@ -109,18 +110,18 @@ public static void main(String[] args) { Options options = new Options(); options.addOption("h", "help", false, "Help"); - options.addOption("d", "Deleted", false, - "Send E-mail report for all bitstreams set as deleted for today"); - options.addOption("m", "Missing", false, - "Send E-mail report for all bitstreams not found in assetstore for today"); - options.addOption("c", "Changed", false, - "Send E-mail report for all bitstreams where checksum has been changed for today"); - options.addOption("a", "All", false, - "Send all E-mail reports"); - options.addOption("u", "Unchecked", false, - "Send the Unchecked bitstream report"); - options.addOption("n", "Not Processed", false, - "Send E-mail report for all bitstreams set to longer be processed for today"); + options.addOption("d", "deleted", false, + "Send email report for all bitstreams set as deleted for today"); + options.addOption("m", "missing", false, + "Send email report for all bitstreams not found in assetstore for today"); + options.addOption("c", "changed", false, + "Send email report for all bitstreams where checksum has been changed for today"); + options.addOption("a", "all", false, + "Send all email reports (used by default)"); + options.addOption("u", "unchecked", false, + "Send the Unchecked bitstream email report"); + options.addOption("n", "not-processed", false, + "Send email report for all bitstreams set to longer be processed for today"); try { line = parser.parse(options, args); @@ -133,13 +134,15 @@ public static void main(String[] args) { if (line.hasOption('h')) { HelpFormatter myhelp = new HelpFormatter(); - myhelp.printHelp("Checksum Reporter\n", options); - System.out.println("\nSend Deleted bitstream email report: DailyReportEmailer -d"); - System.out.println("\nSend Missing bitstreams email report: DailyReportEmailer -m"); - System.out.println("\nSend Checksum Changed email report: DailyReportEmailer -c"); - System.out.println("\nSend bitstream not to be processed email report: DailyReportEmailer -n"); - System.out.println("\nSend Un-checked bitstream report: DailyReportEmailer -u"); - System.out.println("\nSend All email reports: DailyReportEmailer"); + myhelp.printHelp("checker-emailer\n", options); + System.out.println("\nChecksum Checker Reporter usage examples:\n"); + System.out.println(" - Send all email reports: checker-emailer -a"); + System.out.println(" - Send deleted bitstream email report: checker-emailer -d"); + System.out.println(" - Send missing bitstreams email report: checker-emailer -m"); + System.out.println(" - Send checksum changed email report: checker-emailer -c"); + System.out.println(" - Send bitstream not to be processed email report: checker-emailer -n"); + System.out.println(" - Send unchecked bitstream email report: checker-emailer -u"); + System.out.println("\nDefault (no arguments) is equivalent to 'checker-emailer -a'\n"); System.exit(0); } diff --git a/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java b/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java index ddefb28e1b57..6c69764fdc79 100644 --- a/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java @@ -70,6 +70,7 @@ public int getDeletedBitstreamReport(Context context, Date startDate, Date endDa osw.write("\n"); osw.write(msg("deleted-bitstream-intro")); + osw.write(" "); osw.write(applyDateFormatShort(startDate)); osw.write(" "); osw.write(msg("date-range-to")); @@ -111,7 +112,6 @@ public int getChangedChecksumReport(Context context, Date startDate, Date endDat osw.write("\n"); osw.write(msg("checksum-did-not-match")); osw.write(" "); - osw.write("\n"); osw.write(applyDateFormatShort(startDate)); osw.write(" "); osw.write(msg("date-range-to")); diff --git a/dspace-api/src/main/resources/Messages.properties b/dspace-api/src/main/resources/Messages.properties index efbbeedde053..9d15bd0621a8 100644 --- a/dspace-api/src/main/resources/Messages.properties +++ b/dspace-api/src/main/resources/Messages.properties @@ -72,20 +72,20 @@ org.dspace.checker.ResultsLogger.store-number org.dspace.checker.ResultsLogger.to-be-processed = To be processed org.dspace.checker.ResultsLogger.user-format-description = User format description org.dspace.checker.SimpleReporterImpl.bitstream-id = Bitstream Id -org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report for -org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report for +org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report from +org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report from org.dspace.checker.SimpleReporterImpl.check-id = Check Id org.dspace.checker.SimpleReporterImpl.checksum = Checksum org.dspace.checker.SimpleReporterImpl.checksum-algorithm = Checksum Algorithm org.dspace.checker.SimpleReporterImpl.checksum-calculated = Checksum Calculated -org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report for +org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report from org.dspace.checker.SimpleReporterImpl.checksum-expected = Checksum Expected org.dspace.checker.SimpleReporterImpl.date-range-to = to org.dspace.checker.SimpleReporterImpl.deleted = Deleted -org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report for +org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report from org.dspace.checker.SimpleReporterImpl.description = Description org.dspace.checker.SimpleReporterImpl.format-id = Format Id -org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker with the -u option +org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker again org.dspace.checker.SimpleReporterImpl.internal-id = Internal Id org.dspace.checker.SimpleReporterImpl.name = Name org.dspace.checker.SimpleReporterImpl.no-bitstreams-changed = There were no bitstreams found with changed checksums From 5db740aca101d6efd33908cedf39d0014dd05fc8 Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Mon, 18 Aug 2025 12:12:49 +0300 Subject: [PATCH 419/701] Fix sum calculation for checker-emailer and clarify help on not to be processed report (cherry picked from commit 5ebdec62ed27d0c98430a8ff562a1b97da6f6e69) --- .../main/java/org/dspace/checker/DailyReportEmailer.java | 9 ++++++--- .../dspace/checker/service/SimpleReporterService.java | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java index f32ddc232385..ca5733920f6a 100644 --- a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java +++ b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java @@ -119,9 +119,10 @@ public static void main(String[] args) { options.addOption("a", "all", false, "Send all email reports (used by default)"); options.addOption("u", "unchecked", false, - "Send the Unchecked bitstream email report"); + "Send the unchecked (i.e. recently added) bitstream email report"); options.addOption("n", "not-processed", false, - "Send email report for all bitstreams set to longer be processed for today"); + "Send email report for all bitstreams set to no longer be processed for today (includes" + + " bitstreams marked as deleted or not found)"); try { line = parser.parse(options, args); @@ -194,7 +195,9 @@ public static void main(String[] args) { writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); numBitstreams += reporter.getBitstreamNotFoundReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); - numBitstreams += reporter.getNotToBeProcessedReport(context, yesterday, tomorrow, writer); + // not to be processed report includes deleted and not found bitstreams so it is not necessary to + // incude the sum in the counter + reporter.getNotToBeProcessedReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); numBitstreams += reporter.getUncheckedBitstreamsReport(context, writer); writer.write("\n--------------------------------- End Report ---------------------------\n\n"); diff --git a/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java b/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java index 1dc56c20a3de..f3e0b43d8899 100644 --- a/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java +++ b/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java @@ -72,7 +72,8 @@ public int getBitstreamNotFoundReport(Context context, Date startDate, Date endD /** * The bitstreams that were set to not be processed report for the specified - * date range. + * date range. This includes bitstreams that are marked as deleted and bitstreams + * that are not found from the assetstore. * * @param context context * @param startDate the start date range. From 4ecec0950b4e5f7ddc265d8346590a77fb1f8db1 Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Fri, 14 Nov 2025 10:04:56 +0200 Subject: [PATCH 420/701] Fix typo in comment (cherry picked from commit 2aa77e8f27cec22823debc0e57af6e9671843a30) --- .../src/main/java/org/dspace/checker/DailyReportEmailer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java index ca5733920f6a..82c01bf0b9cd 100644 --- a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java +++ b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java @@ -196,7 +196,7 @@ public static void main(String[] args) { numBitstreams += reporter.getBitstreamNotFoundReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); // not to be processed report includes deleted and not found bitstreams so it is not necessary to - // incude the sum in the counter + // include the sum in the counter reporter.getNotToBeProcessedReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); numBitstreams += reporter.getUncheckedBitstreamsReport(context, writer); From 78fd9446d61692768459e9d2690dd4808f754b7f Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Sat, 15 Feb 2025 11:56:57 +0200 Subject: [PATCH 421/701] Clarify command line options help and messages in checker and checker-emailer Based on discussion in #10008 and #8999 with arvoConsultores's original Messages patch. Adds error message if mutually exclusive checker options are attempted to be used in combination. Adds note to log on sent report emails and unifies email report formatting. (cherry picked from commit c5000903617ed10221297df9b00abfbced9b58a5) --- .../dspace/app/checker/ChecksumChecker.java | 53 +++++++++++++------ .../dspace/checker/DailyReportEmailer.java | 41 +++++++------- .../checker/SimpleReporterServiceImpl.java | 2 +- .../src/main/resources/Messages.properties | 10 ++-- 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java b/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java index ec024c345263..160d23e32204 100644 --- a/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java +++ b/dspace-api/src/main/java/org/dspace/app/checker/ChecksumChecker.java @@ -98,7 +98,7 @@ public static void main(String[] args) throws SQLException { options.addOption("h", "help", false, "Help"); options.addOption("d", "duration", true, "Checking duration"); options.addOption("c", "count", true, "Check count"); - options.addOption("a", "handle", true, "Specify a handle to check"); + options.addOption("i", "handle", true, "Specify a handle to check"); options.addOption("v", "verbose", false, "Report all processing"); Option option; @@ -106,7 +106,7 @@ public static void main(String[] args) throws SQLException { option = Option.builder("b") .longOpt("bitstream-ids") .hasArgs() - .desc("Space separated list of bitstream ids") + .desc("Space separated list of bitstream UUIDs") .build(); options.addOption(option); @@ -132,6 +132,17 @@ public static void main(String[] args) throws SQLException { try { context = new Context(); + int mutuallyExclusiveOpts = 0; + for (char c : new char[]{'l', 'L', 'd', 'b', 'i','c'}) { + if (line.hasOption(c)) { + mutuallyExclusiveOpts++; + } + } + if (mutuallyExclusiveOpts > 1) { + System.err.println("Please use only one option of -l, -L, -d, -b, -i, or -c"); + LOG.error("Please use only one option of -l, -L, -d, -b, -i, or -c"); + System.exit(1); + } // Prune stage if (line.hasOption('p')) { @@ -169,13 +180,13 @@ public static void main(String[] args) throws SQLException { bitstreams.add(bitstreamService.find(context, UUID.fromString(ids[i]))); } catch (NumberFormatException nfe) { System.err.println("The following argument: " + ids[i] - + " is not an integer"); + + " is not an UUID"); System.exit(0); } } dispatcher = new IteratorDispatcher(bitstreams.iterator()); - } else if (line.hasOption('a')) { - dispatcher = new HandleDispatcher(context, line.getOptionValue('a')); + } else if (line.hasOption('i')) { + dispatcher = new HandleDispatcher(context, line.getOptionValue('i')); } else if (line.hasOption('d')) { // run checker process for specified duration try { @@ -185,6 +196,8 @@ public static void main(String[] args) throws SQLException { + Utils.parseDuration(line .getOptionValue('d')))); } catch (Exception e) { + System.err.println("Couldn't parse " + line.getOptionValue('d') + + " as a duration"); LOG.fatal("Couldn't parse " + line.getOptionValue('d') + " as a duration: ", e); System.exit(0); @@ -228,18 +241,24 @@ public static void main(String[] args) throws SQLException { private static void printHelp(Options options) { HelpFormatter myhelp = new HelpFormatter(); - myhelp.printHelp("Checksum Checker\n", options); - System.out.println("\nSpecify a duration for checker process, using s(seconds)," - + "m(minutes), or h(hours): ChecksumChecker -d 30s" - + " OR ChecksumChecker -d 30m" - + " OR ChecksumChecker -d 2h"); - System.out.println("\nSpecify bitstream IDs: ChecksumChecker -b 13 15 17 20"); - System.out.println("\nLoop once through all bitstreams: " - + "ChecksumChecker -l"); - System.out.println("\nLoop continuously through all bitstreams: ChecksumChecker -L"); - System.out.println("\nCheck a defined number of bitstreams: ChecksumChecker -c 10"); - System.out.println("\nReport all processing (verbose)(default reports only errors): ChecksumChecker -v"); - System.out.println("\nDefault (no arguments) is equivalent to '-c 1'"); + myhelp.printHelp("checker\n", options); + System.out.println("\nChecksum Checker usage examples:"); + System.out.println("\nThe following options are mutually exclusive:"); + System.out.println(" - Specify a duration for checker process, using s(seconds)," + + "m(minutes), or h(hours): checker -d 30s" + + " OR checker -d 30m" + + " OR checker -d 2h"); + System.out.println(" - Specify bitstream UUIDs: checker -b 550e8400-e29b-41d4-a716-446655440000" + + " f3f2e850-b5d4-11ef-ac7e-96584d5248b2"); + System.out.println(" - Specify handle: checker -i 12345/100"); + System.out.println(" - Loop once through all bitstreams: " + + "checker -l"); + System.out.println(" - Loop continuously through all bitstreams: checker -L"); + System.out.println(" - Check a defined number of bitstreams: checker -c 10"); + System.out.println("\nThe following options can be used in combination with others above:"); + System.out.println(" - Report all processing to checker.log (by default logs only errors): checker -v"); + System.out.println(" - Prune old results from the database: checker -p"); + System.out.println("\nDefault (no arguments) is equivalent to 'checker -c 1'\n"); System.exit(0); } diff --git a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java index 50ef4baa98e3..10892b162831 100644 --- a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java +++ b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java @@ -75,6 +75,7 @@ public void sendReport(File attachment, int numberOfBitstreams) email.setContent("Checker Report", "report is attached ..."); email.addAttachment(attachment, "checksum_checker_report.txt"); email.addRecipient(configurationService.getProperty("mail.admin")); + log.info("Sending checker report email to " + configurationService.getProperty("mail.admin")); email.send(); } } @@ -109,18 +110,18 @@ public static void main(String[] args) { Options options = new Options(); options.addOption("h", "help", false, "Help"); - options.addOption("d", "Deleted", false, - "Send E-mail report for all bitstreams set as deleted for today"); - options.addOption("m", "Missing", false, - "Send E-mail report for all bitstreams not found in assetstore for today"); - options.addOption("c", "Changed", false, - "Send E-mail report for all bitstreams where checksum has been changed for today"); - options.addOption("a", "All", false, - "Send all E-mail reports"); - options.addOption("u", "Unchecked", false, - "Send the Unchecked bitstream report"); - options.addOption("n", "Not Processed", false, - "Send E-mail report for all bitstreams set to longer be processed for today"); + options.addOption("d", "deleted", false, + "Send email report for all bitstreams set as deleted for today"); + options.addOption("m", "missing", false, + "Send email report for all bitstreams not found in assetstore for today"); + options.addOption("c", "changed", false, + "Send email report for all bitstreams where checksum has been changed for today"); + options.addOption("a", "all", false, + "Send all email reports (used by default)"); + options.addOption("u", "unchecked", false, + "Send the Unchecked bitstream email report"); + options.addOption("n", "not-processed", false, + "Send email report for all bitstreams set to longer be processed for today"); try { line = parser.parse(options, args); @@ -133,13 +134,15 @@ public static void main(String[] args) { if (line.hasOption('h')) { HelpFormatter myhelp = new HelpFormatter(); - myhelp.printHelp("Checksum Reporter\n", options); - System.out.println("\nSend Deleted bitstream email report: DailyReportEmailer -d"); - System.out.println("\nSend Missing bitstreams email report: DailyReportEmailer -m"); - System.out.println("\nSend Checksum Changed email report: DailyReportEmailer -c"); - System.out.println("\nSend bitstream not to be processed email report: DailyReportEmailer -n"); - System.out.println("\nSend Un-checked bitstream report: DailyReportEmailer -u"); - System.out.println("\nSend All email reports: DailyReportEmailer"); + myhelp.printHelp("checker-emailer\n", options); + System.out.println("\nChecksum Checker Reporter usage examples:\n"); + System.out.println(" - Send all email reports: checker-emailer -a"); + System.out.println(" - Send deleted bitstream email report: checker-emailer -d"); + System.out.println(" - Send missing bitstreams email report: checker-emailer -m"); + System.out.println(" - Send checksum changed email report: checker-emailer -c"); + System.out.println(" - Send bitstream not to be processed email report: checker-emailer -n"); + System.out.println(" - Send unchecked bitstream email report: checker-emailer -u"); + System.out.println("\nDefault (no arguments) is equivalent to 'checker-emailer -a'\n"); System.exit(0); } diff --git a/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java b/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java index ddefb28e1b57..6c69764fdc79 100644 --- a/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/checker/SimpleReporterServiceImpl.java @@ -70,6 +70,7 @@ public int getDeletedBitstreamReport(Context context, Date startDate, Date endDa osw.write("\n"); osw.write(msg("deleted-bitstream-intro")); + osw.write(" "); osw.write(applyDateFormatShort(startDate)); osw.write(" "); osw.write(msg("date-range-to")); @@ -111,7 +112,6 @@ public int getChangedChecksumReport(Context context, Date startDate, Date endDat osw.write("\n"); osw.write(msg("checksum-did-not-match")); osw.write(" "); - osw.write("\n"); osw.write(applyDateFormatShort(startDate)); osw.write(" "); osw.write(msg("date-range-to")); diff --git a/dspace-api/src/main/resources/Messages.properties b/dspace-api/src/main/resources/Messages.properties index efbbeedde053..9d15bd0621a8 100644 --- a/dspace-api/src/main/resources/Messages.properties +++ b/dspace-api/src/main/resources/Messages.properties @@ -72,20 +72,20 @@ org.dspace.checker.ResultsLogger.store-number org.dspace.checker.ResultsLogger.to-be-processed = To be processed org.dspace.checker.ResultsLogger.user-format-description = User format description org.dspace.checker.SimpleReporterImpl.bitstream-id = Bitstream Id -org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report for -org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report for +org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report from +org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report from org.dspace.checker.SimpleReporterImpl.check-id = Check Id org.dspace.checker.SimpleReporterImpl.checksum = Checksum org.dspace.checker.SimpleReporterImpl.checksum-algorithm = Checksum Algorithm org.dspace.checker.SimpleReporterImpl.checksum-calculated = Checksum Calculated -org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report for +org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report from org.dspace.checker.SimpleReporterImpl.checksum-expected = Checksum Expected org.dspace.checker.SimpleReporterImpl.date-range-to = to org.dspace.checker.SimpleReporterImpl.deleted = Deleted -org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report for +org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report from org.dspace.checker.SimpleReporterImpl.description = Description org.dspace.checker.SimpleReporterImpl.format-id = Format Id -org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker with the -u option +org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker again org.dspace.checker.SimpleReporterImpl.internal-id = Internal Id org.dspace.checker.SimpleReporterImpl.name = Name org.dspace.checker.SimpleReporterImpl.no-bitstreams-changed = There were no bitstreams found with changed checksums From 1e0b5a26e6353f0c456e81bd6067b7944c0d6b17 Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Mon, 18 Aug 2025 12:12:49 +0300 Subject: [PATCH 422/701] Fix sum calculation for checker-emailer and clarify help on not to be processed report (cherry picked from commit 5ebdec62ed27d0c98430a8ff562a1b97da6f6e69) --- .../main/java/org/dspace/checker/DailyReportEmailer.java | 9 ++++++--- .../dspace/checker/service/SimpleReporterService.java | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java index 10892b162831..f353e0690b17 100644 --- a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java +++ b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java @@ -119,9 +119,10 @@ public static void main(String[] args) { options.addOption("a", "all", false, "Send all email reports (used by default)"); options.addOption("u", "unchecked", false, - "Send the Unchecked bitstream email report"); + "Send the unchecked (i.e. recently added) bitstream email report"); options.addOption("n", "not-processed", false, - "Send email report for all bitstreams set to longer be processed for today"); + "Send email report for all bitstreams set to no longer be processed for today (includes" + + " bitstreams marked as deleted or not found)"); try { line = parser.parse(options, args); @@ -194,7 +195,9 @@ public static void main(String[] args) { writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); numBitstreams += reporter.getBitstreamNotFoundReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); - numBitstreams += reporter.getNotToBeProcessedReport(context, yesterday, tomorrow, writer); + // not to be processed report includes deleted and not found bitstreams so it is not necessary to + // incude the sum in the counter + reporter.getNotToBeProcessedReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); numBitstreams += reporter.getUncheckedBitstreamsReport(context, writer); writer.write("\n--------------------------------- End Report ---------------------------\n\n"); diff --git a/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java b/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java index 1dc56c20a3de..f3e0b43d8899 100644 --- a/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java +++ b/dspace-api/src/main/java/org/dspace/checker/service/SimpleReporterService.java @@ -72,7 +72,8 @@ public int getBitstreamNotFoundReport(Context context, Date startDate, Date endD /** * The bitstreams that were set to not be processed report for the specified - * date range. + * date range. This includes bitstreams that are marked as deleted and bitstreams + * that are not found from the assetstore. * * @param context context * @param startDate the start date range. From 57b101b5d597e179890d113d4be4f3ed1e75d06e Mon Sep 17 00:00:00 2001 From: Miika Nurminen Date: Fri, 14 Nov 2025 10:04:56 +0200 Subject: [PATCH 423/701] Fix typo in comment (cherry picked from commit 2aa77e8f27cec22823debc0e57af6e9671843a30) --- .../src/main/java/org/dspace/checker/DailyReportEmailer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java index f353e0690b17..481b055fbb7d 100644 --- a/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java +++ b/dspace-api/src/main/java/org/dspace/checker/DailyReportEmailer.java @@ -196,7 +196,7 @@ public static void main(String[] args) { numBitstreams += reporter.getBitstreamNotFoundReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); // not to be processed report includes deleted and not found bitstreams so it is not necessary to - // incude the sum in the counter + // include the sum in the counter reporter.getNotToBeProcessedReport(context, yesterday, tomorrow, writer); writer.write("\n--------------------------------- Report Spacer ---------------------------\n\n"); numBitstreams += reporter.getUncheckedBitstreamsReport(context, writer); From ef8f4383b01f7110f5a91c18fb63a5eb260429d1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 2 Oct 2025 16:12:51 -0500 Subject: [PATCH 424/701] Remove X509Authentication, its configuration, and all references to it within other files. --- .../authenticate/AuthenticationMethod.java | 8 +- .../AuthenticationServiceImpl.java | 8 +- .../authenticate/LDAPAuthentication.java | 2 +- .../authenticate/PasswordAuthentication.java | 4 +- .../authenticate/ShibAuthentication.java | 7 +- .../authenticate/X509Authentication.java | 616 ------------------ .../service/AuthenticationService.java | 12 +- .../org/dspace/eperson/EPersonCLITool.java | 2 +- dspace/config/dspace.cfg | 1 - dspace/config/local.cfg.EXAMPLE | 3 - dspace/config/modules/authentication-x509.cfg | 23 - dspace/config/modules/authentication.cfg | 6 - 12 files changed, 21 insertions(+), 671 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java delete mode 100644 dspace/config/modules/authentication-x509.cfg diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java index d316cb636f87..9f1c0827e979 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java @@ -54,7 +54,7 @@ public interface AuthenticationMethod { public static final int BAD_CREDENTIALS = 2; /** - * Not allowed to login this way without X.509 certificate. + * Not allowed to login this way without a certificate. */ public static final int CERT_REQUIRED = 3; @@ -124,8 +124,8 @@ public boolean allowSetPassword(Context context, * Predicate, is this an implicit authentication method. * An implicit method gets credentials from the environment (such as * an HTTP request or even Java system properties) rather than the - * explicit username and password. For example, a method that reads - * the X.509 certificates in an HTTPS request is implicit. + * explicit username and password. For example, a method that provides + * IP-based authentication is implicit. * * @return true if this method uses implicit authentication. */ @@ -188,7 +188,7 @@ public default boolean areSpecialGroupsApplicable(Context context, HttpServletRe *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/pw not appropriate for this method * @throws SQLException if database error diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java index 2b07f73c489c..9387faeb738e 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java @@ -38,11 +38,11 @@ * Configuration
* The stack of authentication methods is defined by one property in the DSpace configuration: *

- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = a list of method class names
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = a list of method class names
  *     e.g.
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = \
- *       org.dspace.eperson.X509Authentication, \
- *       org.dspace.eperson.PasswordAuthentication
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = \
+ *       org.dspace.authenticate.IPAuthentication, \
+ *       org.dspace.authenticate.PasswordAuthentication
  * 
*

* The "stack" is always traversed in order, with the methods diff --git a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java index 40b8f48078c9..9d7c09f1c314 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java @@ -203,7 +203,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/pw not appropriate for this method */ diff --git a/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java index 8e030305c957..035a235422a6 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java @@ -188,7 +188,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but password doesn't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - no EPerson with matching email address. *
BAD_ARGS - missing username, or user matched but cannot login. * @throws SQLException if database error @@ -213,7 +213,7 @@ public int authenticate(Context context, // cannot login this way return BAD_ARGS; } else if (eperson.getRequireCertificate()) { - // this user can only login with x.509 certificate + // this user can only login with a certificate log.warn(LogHelper.getHeader(context, "authenticate", "rejecting PasswordAuthentication because " + username + " requires " + "certificate.")); diff --git a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java index 24d8266012d4..b5eed7e764b0 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java @@ -160,7 +160,7 @@ public class ShibAuthentication implements AuthenticationMethod { * SUCCESS - authenticated OK.
* BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) * don't match
- * CERT_REQUIRED - not allowed to login this way without X.509 cert. + * CERT_REQUIRED - not allowed to login this way without a cert. *
* NO_SUCH_USER - user not found using this method.
* BAD_ARGS - user/pw not appropriate for this method @@ -417,8 +417,7 @@ public boolean allowSetPassword(Context context, * Predicate, is this an implicit authentication method. An implicit method * gets credentials from the environment (such as an HTTP request or even * Java system properties) rather than the explicit username and password. - * For example, a method that reads the X.509 certificates in an HTTPS - * request is implicit. + * For example, a method that provides IP-based authentication is implicit. * * @return true if this method uses implicit authentication. */ @@ -917,7 +916,7 @@ protected int swordCompatibility(Context context, String username, String passwo " is not allowed to login."); return BAD_ARGS; } else if (eperson.getRequireCertificate()) { - // this user can only login with x.509 certificate + // this user can only login with a certificate log.error( "Shibboleth-based password authentication failed for user " + username + " because the eperson object" + " requires a certificate to authenticate.."); diff --git a/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java b/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java deleted file mode 100644 index 55843c710760..000000000000 --- a/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java +++ /dev/null @@ -1,616 +0,0 @@ -/** - * 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.authenticate; - -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.Principal; -import java.security.PublicKey; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.StringTokenizer; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.logging.log4j.Logger; -import org.dspace.authenticate.factory.AuthenticateServiceFactory; -import org.dspace.authenticate.service.AuthenticationService; -import org.dspace.authorize.AuthorizeException; -import org.dspace.core.Context; -import org.dspace.core.LogHelper; -import org.dspace.eperson.EPerson; -import org.dspace.eperson.Group; -import org.dspace.eperson.factory.EPersonServiceFactory; -import org.dspace.eperson.service.EPersonService; -import org.dspace.eperson.service.GroupService; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; - -/** - * Implicit authentication method that gets credentials from the X.509 client - * certificate supplied by the HTTPS client when connecting to this server. The - * email address in that certificate is taken as the authenticated user name - * with no further checking, so be sure your HTTP server (e.g. Tomcat) is - * configured correctly to accept only client certificates it can validate. - *

- * See the AuthenticationMethod interface for more details. - *

- * Configuration: - * - *

- *   x509.keystore.path =
- * 
- * path to Java keystore file
- * 
- *   keystore.password =
- * 
- * password to access the keystore
- * 
- *   ca.cert =
- * 
- * path to certificate file for CA whose client certs to accept.
- * 
- *   autoregister =
- * 
- * "true" if E-Person is created automatically for unknown new users.
- * 
- *   groups =
- * 
- * comma-delimited list of special groups to add user to if authenticated.
- * 
- *   emaildomain =
- * 
- * email address domain (after the 'at' symbol) to match before allowing
- * membership in special groups.
- * 
- * 
- * - * Only one of the "keystore.path" or "ca.cert" - * options is required. If you supply a keystore, then all of the "trusted" - * certificates in the keystore represent CAs whose client certificates will be - * accepted. The ca.cert option only allows a single CA to be - * named. - *

- * You can configure both a keystore and a CA cert, and both will be - * used. - *

- * The autoregister configuration parameter determines what the - * canSelfRegister() method returns. It also allows an EPerson - * record to be created automatically when the presented certificate is - * acceptable but there is no corresponding EPerson. - * - * @author Larry Stone - * @version $Revision$ - */ -public class X509Authentication implements AuthenticationMethod { - - /** - * log4j category - */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(X509Authentication.class); - - /** - * public key of CA to check client certs against. - */ - private static PublicKey caPublicKey = null; - - /** - * key store for CA certs if we use that - */ - private static KeyStore caCertKeyStore = null; - - private static String loginPageTitle = null; - - private static String loginPageURL = null; - - protected AuthenticationService authenticationService = AuthenticateServiceFactory.getInstance() - .getAuthenticationService(); - protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); - protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); - protected ConfigurationService configurationService = - DSpaceServicesFactory.getInstance().getConfigurationService(); - - private static final String X509_AUTHENTICATED = "x509.authenticated"; - - - /** - * Initialization: Set caPublicKey and/or keystore. This loads the - * information needed to check if a client cert presented is valid and - * acceptable. - */ - static { - ConfigurationService configurationService = - DSpaceServicesFactory.getInstance().getConfigurationService(); - /* - * allow identification of alternative entry points for certificate - * authentication when selected by the user rather than implicitly. - */ - loginPageTitle = configurationService - .getProperty("authentication-x509.chooser.title.key"); - loginPageURL = configurationService - .getProperty("authentication-x509.chooser.uri"); - - String keystorePath = configurationService - .getProperty("authentication-x509.keystore.path"); - String keystorePassword = configurationService - .getProperty("authentication-x509.keystore.password"); - String caCertPath = configurationService - .getProperty("authentication-x509.ca.cert"); - - // First look for keystore full of trusted certs. - if (keystorePath != null) { - FileInputStream fis = null; - if (keystorePassword == null) { - keystorePassword = ""; - } - try { - KeyStore ks = KeyStore.getInstance("JKS"); - fis = new FileInputStream(keystorePath); - ks.load(fis, keystorePassword.toCharArray()); - caCertKeyStore = ks; - } catch (IOException e) { - log - .error("X509Authentication: Failed to load CA keystore, file=" - + keystorePath + ", error=" + e.toString()); - } catch (GeneralSecurityException e) { - log - .error("X509Authentication: Failed to extract CA keystore, file=" - + keystorePath + ", error=" + e.toString()); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException ioe) { - // ignore - } - } - } - } - - // Second, try getting public key out of CA cert, if that's configured. - if (caCertPath != null) { - InputStream is = null; - FileInputStream fis = null; - try { - fis = new FileInputStream(caCertPath); - is = new BufferedInputStream(fis); - X509Certificate cert = (X509Certificate) CertificateFactory - .getInstance("X.509").generateCertificate(is); - if (cert != null) { - caPublicKey = cert.getPublicKey(); - } - } catch (IOException e) { - log.error("X509Authentication: Failed to load CA cert, file=" - + caCertPath + ", error=" + e.toString()); - } catch (CertificateException e) { - log - .error("X509Authentication: Failed to extract CA cert, file=" - + caCertPath + ", error=" + e.toString()); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException ioe) { - // ignore - } - } - - if (fis != null) { - try { - fis.close(); - } catch (IOException ioe) { - // ignore - } - } - } - } - } - - /** - * Return the email address from certificate, or null if an - * email address cannot be found in the certificate. - *

- * Note that the certificate parsing has only been tested with certificates - * granted by the MIT Certification Authority, and may not work elsewhere. - * - * @param certificate - - * An X509 certificate object - * @return - The email address found in certificate, or null if an email - * address cannot be found in the certificate. - */ - private static String getEmail(X509Certificate certificate) - throws SQLException { - Principal principal = certificate.getSubjectDN(); - - if (principal == null) { - return null; - } - - String dn = principal.getName(); - if (dn == null) { - return null; - } - - StringTokenizer tokenizer = new StringTokenizer(dn, ","); - String token = null; - while (tokenizer.hasMoreTokens()) { - int len = "emailaddress=".length(); - - token = (String) tokenizer.nextToken(); - - if (token.toLowerCase().startsWith("emailaddress=")) { - // Make sure the token actually contains something - if (token.length() <= len) { - return null; - } - - return token.substring(len).toLowerCase(); - } - } - - return null; - } - - /** - * Verify CERTIFICATE against KEY. Return true if and only if CERTIFICATE is - * valid and can be verified against KEY. - * - * @param context The current DSpace context - * @param certificate - - * An X509 certificate object - * @return - True if CERTIFICATE is valid and can be verified against KEY, - * false otherwise. - */ - private static boolean isValid(Context context, X509Certificate certificate) { - if (certificate == null) { - return false; - } - - // This checks that current time is within cert's validity window: - try { - certificate.checkValidity(); - } catch (CertificateException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate is EXPIRED or PREMATURE: " - + e.toString())); - return false; - } - - // Try CA public key, if available. - if (caPublicKey != null) { - try { - certificate.verify(caPublicKey); - return true; - } catch (GeneralSecurityException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate FAILED SIGNATURE check: " - + e.toString())); - } - } - - // Try it with keystore, if available. - if (caCertKeyStore != null) { - try { - Enumeration ke = caCertKeyStore.aliases(); - - while (ke.hasMoreElements()) { - String alias = (String) ke.nextElement(); - if (caCertKeyStore.isCertificateEntry(alias)) { - Certificate ca = caCertKeyStore.getCertificate(alias); - try { - certificate.verify(ca.getPublicKey()); - return true; - } catch (CertificateException ce) { - // ignore - } - } - } - log - .info(LogHelper - .getHeader(context, "authentication", - "Keystore method FAILED SIGNATURE check on client cert.")); - } catch (GeneralSecurityException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate FAILED SIGNATURE check: " - + e.toString())); - } - - } - return false; - } - - /** - * Predicate, can new user automatically create EPerson. Checks - * configuration value. You'll probably want this to be true to take - * advantage of a Web certificate infrastructure with many more users than - * are already known by DSpace. - * - * @throws SQLException if database error - */ - @Override - public boolean canSelfRegister(Context context, HttpServletRequest request, - String username) throws SQLException { - return configurationService - .getBooleanProperty("authentication-x509.autoregister"); - } - - /** - * Nothing extra to initialize. - * - * @throws SQLException if database error - */ - @Override - public void initEPerson(Context context, HttpServletRequest request, - EPerson eperson) throws SQLException { - } - - /** - * We don't use EPerson password so there is no reason to change it. - * - * @throws SQLException if database error - */ - @Override - public boolean allowSetPassword(Context context, - HttpServletRequest request, String username) throws SQLException { - return false; - } - - /** - * Returns true, this is an implicit method. - */ - @Override - public boolean isImplicit() { - return true; - } - - /** - * Returns a list of group names that the user should be added to upon - * successful authentication, configured in dspace.cfg. - * - * @return List of special groups configured for this authenticator - */ - private List getX509Groups() { - List groupNames = new ArrayList(); - - String[] groups = configurationService - .getArrayProperty("authentication-x509.groups"); - - if (ArrayUtils.isNotEmpty(groups)) { - for (String group : groups) { - groupNames.add(group.trim()); - } - } - - return groupNames; - } - - /** - * Checks for configured email domain required to grant special groups - * membership. If no email domain is configured to verify, special group - * membership is simply granted. - * - * @param request - - * The current request object - * @param email - - * The email address from the x509 certificate - */ - private void setSpecialGroupsFlag(HttpServletRequest request, String email) { - String emailDomain = null; - emailDomain = (String) request - .getAttribute("authentication.x509.emaildomain"); - - HttpSession session = request.getSession(true); - - if (null != emailDomain && !"".equals(emailDomain)) { - if (email.substring(email.length() - emailDomain.length()).equals( - emailDomain)) { - session.setAttribute("x509Auth", Boolean.TRUE); - } - } else { - // No configured email domain to verify. Just flag - // as authenticated so special groups are granted. - session.setAttribute("x509Auth", Boolean.TRUE); - } - } - - /** - * Return special groups configured in dspace.cfg for X509 certificate - * authentication. - * - * @param context context - * @param request object potentially containing the cert - * @return An int array of group IDs - * @throws SQLException if database error - */ - @Override - public List getSpecialGroups(Context context, HttpServletRequest request) - throws SQLException { - if (request == null) { - return Collections.EMPTY_LIST; - } - - Boolean authenticated = false; - HttpSession session = request.getSession(false); - authenticated = (Boolean) session.getAttribute("x509Auth"); - authenticated = (null == authenticated) ? false : authenticated; - - if (authenticated) { - List groupNames = getX509Groups(); - List groups = new ArrayList<>(); - - if (groupNames != null) { - for (String groupName : groupNames) { - if (groupName != null) { - Group group = groupService.findByName(context, groupName); - if (group != null) { - groups.add(group); - } else { - log.warn(LogHelper.getHeader(context, - "configuration_error", "unknown_group=" - + groupName)); - } - } - } - } - - return groups; - } - - return Collections.EMPTY_LIST; - } - - /** - * X509 certificate authentication. The client certificate is obtained from - * the ServletRequest object. - *

    - *
  • If the certificate is valid, and corresponds to an existing EPerson, - * and the user is allowed to login, return success.
  • - *
  • If the user is matched but is not allowed to login, it fails.
  • - *
  • If the certificate is valid, but there is no corresponding EPerson, - * the "authentication.x509.autoregister" configuration - * parameter is checked (via canSelfRegister()) - *
      - *
    • If it's true, a new EPerson record is created for the certificate, - * and the result is success.
    • - *
    • If it's false, return that the user was unknown.
    • - *
    - *
  • - *
- * - * @return One of: SUCCESS, BAD_CREDENTIALS, NO_SUCH_USER, BAD_ARGS - * @throws SQLException if database error - */ - @Override - public int authenticate(Context context, String username, String password, - String realm, HttpServletRequest request) throws SQLException { - // Obtain the certificate from the request, if any - X509Certificate[] certs = null; - if (request != null) { - certs = (X509Certificate[]) request - .getAttribute("jakarta.servlet.request.X509Certificate"); - } - - if ((certs == null) || (certs.length == 0)) { - return BAD_ARGS; - } else { - // We have a cert -- check it and get username from it. - try { - if (!isValid(context, certs[0])) { - log - .warn(LogHelper - .getHeader(context, "authenticate", - "type=x509certificate, status=BAD_CREDENTIALS (not valid)")); - return BAD_CREDENTIALS; - } - - // And it's valid - try and get an e-person - String email = getEmail(certs[0]); - EPerson eperson = null; - if (email != null) { - eperson = ePersonService.findByEmail(context, email); - } - if (eperson == null) { - // Cert is valid, but no record. - if (email != null - && canSelfRegister(context, request, null)) { - // Register the new user automatically - log.info(LogHelper.getHeader(context, "autoregister", - "from=x.509, email=" + email)); - - // TEMPORARILY turn off authorisation - context.turnOffAuthorisationSystem(); - eperson = ePersonService.create(context); - eperson.setEmail(email); - eperson.setCanLogIn(true); - authenticationService.initEPerson(context, request, - eperson); - ePersonService.update(context, eperson); - context.dispatchEvents(); - context.restoreAuthSystemState(); - context.setCurrentUser(eperson); - request.setAttribute(X509_AUTHENTICATED, true); - setSpecialGroupsFlag(request, email); - return SUCCESS; - } else { - // No auto-registration for valid certs - log - .warn(LogHelper - .getHeader(context, "authenticate", - "type=cert_but_no_record, cannot auto-register")); - return NO_SUCH_USER; - } - } else if (!eperson.canLogIn()) { // make sure this is a login account - log.warn(LogHelper.getHeader(context, "authenticate", - "type=x509certificate, email=" + email - + ", canLogIn=false, rejecting.")); - return BAD_ARGS; - } else { - log.info(LogHelper.getHeader(context, "login", - "type=x509certificate")); - context.setCurrentUser(eperson); - request.setAttribute(X509_AUTHENTICATED, true); - setSpecialGroupsFlag(request, email); - return SUCCESS; - } - } catch (AuthorizeException ce) { - log.warn(LogHelper.getHeader(context, "authorize_exception", - ""), ce); - } - - return BAD_ARGS; - } - } - - /** - * Returns URL of password-login servlet. - * - * @param context DSpace context, will be modified (EPerson set) upon success. - * @param request The HTTP request that started this operation, or null if not - * applicable. - * @param response The HTTP response from the servlet method. - * @return fully-qualified URL - */ - @Override - public String loginPageURL(Context context, HttpServletRequest request, - HttpServletResponse response) { - return loginPageURL; - } - - @Override - public String getName() { - return "x509"; - } - - @Override - public boolean isUsed(final Context context, final HttpServletRequest request) { - if (request != null && - context.getCurrentUser() != null && - request.getAttribute(X509_AUTHENTICATED) != null) { - return true; - } - return false; - } - - @Override - public boolean canChangePassword(Context context, EPerson ePerson, String currentPassword) { - return false; - } -} diff --git a/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java b/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java index 45ad8932daec..8409f1e27d05 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java @@ -29,11 +29,11 @@ * Configuration
* The stack of authentication methods is defined by one property in the DSpace configuration: *
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = a list of method class names
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = a list of method class names
  *     e.g.
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = \
- *       org.dspace.eperson.X509Authentication, \
- *       org.dspace.eperson.PasswordAuthentication
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = \
+ *       org.dspace.authenticate.IPAuthentication, \
+ *       org.dspace.authenticate.PasswordAuthentication
  * 
*

* The "stack" is always traversed in order, with the methods @@ -64,7 +64,7 @@ public interface AuthenticationService { *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. password) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/password not appropriate for this method */ @@ -91,7 +91,7 @@ public int authenticate(Context context, *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. password) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/password not appropriate for this method */ diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java b/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java index 343ddcccfa39..53451e07a9ec 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java @@ -50,7 +50,7 @@ public class EPersonCLITool { private static final Option OPT_PHONE = new Option("t", "telephone", true, "telephone number, empty for none"); private static final Option OPT_LANGUAGE = new Option("l", "language", true, "the person's preferred language"); private static final Option OPT_REQUIRE_CERTIFICATE = new Option("c", "requireCertificate", true, - "if 'true', an X.509 certificate will be " + + "if 'true', a certificate will be " + "required for login"); private static final Option OPT_CAN_LOGIN = new Option("C", "canLogIn", true, "'true' if the user can log in"); diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 67490dac3e6d..01e168937a99 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1655,7 +1655,6 @@ include = ${module_dir}/authentication-ldap.cfg include = ${module_dir}/authentication-oidc.cfg include = ${module_dir}/authentication-password.cfg include = ${module_dir}/authentication-shibboleth.cfg -include = ${module_dir}/authentication-x509.cfg include = ${module_dir}/authority.cfg include = ${module_dir}/bulkedit.cfg include = ${module_dir}/citation-page.cfg diff --git a/dspace/config/local.cfg.EXAMPLE b/dspace/config/local.cfg.EXAMPLE index 7454fcde06b4..15168a911336 100644 --- a/dspace/config/local.cfg.EXAMPLE +++ b/dspace/config/local.cfg.EXAMPLE @@ -200,9 +200,6 @@ db.password = dspace # ORCID certificate authentication. # plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.OrcidAuthentication -# X.509 certificate authentication. See authentication-x509.cfg for default configuration. -#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.X509Authentication - # Authentication by Password (encrypted in DSpace's database). See authentication-password.cfg for default configuration. # Enabled by default in authentication.cfg #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.PasswordAuthentication diff --git a/dspace/config/modules/authentication-x509.cfg b/dspace/config/modules/authentication-x509.cfg deleted file mode 100644 index d3f05c7d17d5..000000000000 --- a/dspace/config/modules/authentication-x509.cfg +++ /dev/null @@ -1,23 +0,0 @@ -#---------------------------------------------------------------# -#------X.509 CERTIFICATE AUTHENTICATION CONFIGURATIONS----------# -#---------------------------------------------------------------# -# Configuration properties used by the X.509 Certificate # -# Authentication plugin, when it is enabled. # -#---------------------------------------------------------------# - -## method 1, using keystore -#authentication-x509.keystore.path = /tomcat/conf/keystore -#authentication-x509.keystore.password = changeit - -## method 2, using CA certificate -#authentication-x509.ca.cert = ${dspace.dir}/config/MyClientCA.pem - -## Create e-persons for unknown names in valid certificates? -#authentication-x509.autoregister = true - -## Allow Certificate auth to show as a choice in chooser -# Use Messages.properties key for title -#authentication-x509.chooser.title.key=org.dspace.eperson.X509Authentication.title -# -# Identify the location of the Certificate Login Servlet. -#authentication-x509.chooser.uri=/certificate-login diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index 568f871e3cd7..41c28df1d7c9 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -21,9 +21,6 @@ # * IP Address Authentication # Plugin class: org.dspace.authenticate.IPAuthentication # Configuration file: authentication-ip.cfg -# * X.509 Certificate Authentication -# Plugin class: org.dspace.authenticate.X509Authentication -# Configuration file: authentication-x509.cfg # * ORCID certificate authentication. # Plugin class: org.dspace.authenticate.OrcidAuthentication # Configuration file: orcid.cfg @@ -49,9 +46,6 @@ # Shibboleth authentication/authorization. See authentication-shibboleth.cfg for default configuration. #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.ShibAuthentication -# X.509 certificate authentication. See authentication-x509.cfg for default configuration. -#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.X509Authentication - # ORCID certificate authentication. # plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.OrcidAuthentication From 1a5123de8375e590ec7dd5a4e0ec3aeac5d402d8 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 2 Oct 2025 16:12:51 -0500 Subject: [PATCH 425/701] Remove X509Authentication, its configuration, and all references to it within other files. --- .../authenticate/AuthenticationMethod.java | 8 +- .../AuthenticationServiceImpl.java | 8 +- .../authenticate/LDAPAuthentication.java | 2 +- .../authenticate/PasswordAuthentication.java | 4 +- .../authenticate/ShibAuthentication.java | 7 +- .../authenticate/X509Authentication.java | 616 ------------------ .../service/AuthenticationService.java | 12 +- .../org/dspace/eperson/EPersonCLITool.java | 2 +- dspace/config/dspace.cfg | 1 - dspace/config/local.cfg.EXAMPLE | 3 - dspace/config/modules/authentication-x509.cfg | 23 - dspace/config/modules/authentication.cfg | 6 - 12 files changed, 21 insertions(+), 671 deletions(-) delete mode 100644 dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java delete mode 100644 dspace/config/modules/authentication-x509.cfg diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java index 500ee04a979b..38488c41e302 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java @@ -54,7 +54,7 @@ public interface AuthenticationMethod { public static final int BAD_CREDENTIALS = 2; /** - * Not allowed to login this way without X.509 certificate. + * Not allowed to login this way without a certificate. */ public static final int CERT_REQUIRED = 3; @@ -124,8 +124,8 @@ public boolean allowSetPassword(Context context, * Predicate, is this an implicit authentication method. * An implicit method gets credentials from the environment (such as * an HTTP request or even Java system properties) rather than the - * explicit username and password. For example, a method that reads - * the X.509 certificates in an HTTPS request is implicit. + * explicit username and password. For example, a method that provides + * IP-based authentication is implicit. * * @return true if this method uses implicit authentication. */ @@ -188,7 +188,7 @@ public default boolean areSpecialGroupsApplicable(Context context, HttpServletRe *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/pw not appropriate for this method * @throws SQLException if database error diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java index 1d67da37ecb3..493e4ade4168 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java @@ -38,11 +38,11 @@ * Configuration
* The stack of authentication methods is defined by one property in the DSpace configuration: *

- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = a list of method class names
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = a list of method class names
  *     e.g.
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = \
- *       org.dspace.eperson.X509Authentication, \
- *       org.dspace.eperson.PasswordAuthentication
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = \
+ *       org.dspace.authenticate.IPAuthentication, \
+ *       org.dspace.authenticate.PasswordAuthentication
  * 
*

* The "stack" is always traversed in order, with the methods diff --git a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java index da6a70924818..564ad78c951c 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java @@ -203,7 +203,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/pw not appropriate for this method */ diff --git a/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java index 6d1ca862d307..69f0bd55f870 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/PasswordAuthentication.java @@ -188,7 +188,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but password doesn't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - no EPerson with matching email address. *
BAD_ARGS - missing username, or user matched but cannot login. * @throws SQLException if database error @@ -213,7 +213,7 @@ public int authenticate(Context context, // cannot login this way return BAD_ARGS; } else if (eperson.getRequireCertificate()) { - // this user can only login with x.509 certificate + // this user can only login with a certificate log.warn(LogHelper.getHeader(context, "authenticate", "rejecting PasswordAuthentication because " + username + " requires " + "certificate.")); diff --git a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java index 791634a7dc25..ea6dd0347ee7 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java @@ -160,7 +160,7 @@ public class ShibAuthentication implements AuthenticationMethod { * SUCCESS - authenticated OK.
* BAD_CREDENTIALS - user exists, but credentials (e.g. passwd) * don't match
- * CERT_REQUIRED - not allowed to login this way without X.509 cert. + * CERT_REQUIRED - not allowed to login this way without a cert. *
* NO_SUCH_USER - user not found using this method.
* BAD_ARGS - user/pw not appropriate for this method @@ -417,8 +417,7 @@ public boolean allowSetPassword(Context context, * Predicate, is this an implicit authentication method. An implicit method * gets credentials from the environment (such as an HTTP request or even * Java system properties) rather than the explicit username and password. - * For example, a method that reads the X.509 certificates in an HTTPS - * request is implicit. + * For example, a method that provides IP-based authentication is implicit. * * @return true if this method uses implicit authentication. */ @@ -917,7 +916,7 @@ protected int swordCompatibility(Context context, String username, String passwo " is not allowed to login."); return BAD_ARGS; } else if (eperson.getRequireCertificate()) { - // this user can only login with x.509 certificate + // this user can only login with a certificate log.error( "Shibboleth-based password authentication failed for user " + username + " because the eperson object" + " requires a certificate to authenticate.."); diff --git a/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java b/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java deleted file mode 100644 index 12dc5feda583..000000000000 --- a/dspace-api/src/main/java/org/dspace/authenticate/X509Authentication.java +++ /dev/null @@ -1,616 +0,0 @@ -/** - * 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.authenticate; - -import java.io.BufferedInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.Principal; -import java.security.PublicKey; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.StringTokenizer; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.logging.log4j.Logger; -import org.dspace.authenticate.factory.AuthenticateServiceFactory; -import org.dspace.authenticate.service.AuthenticationService; -import org.dspace.authorize.AuthorizeException; -import org.dspace.core.Context; -import org.dspace.core.LogHelper; -import org.dspace.eperson.EPerson; -import org.dspace.eperson.Group; -import org.dspace.eperson.factory.EPersonServiceFactory; -import org.dspace.eperson.service.EPersonService; -import org.dspace.eperson.service.GroupService; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; - -/** - * Implicit authentication method that gets credentials from the X.509 client - * certificate supplied by the HTTPS client when connecting to this server. The - * email address in that certificate is taken as the authenticated user name - * with no further checking, so be sure your HTTP server (e.g. Tomcat) is - * configured correctly to accept only client certificates it can validate. - *

- * See the AuthenticationMethod interface for more details. - *

- * Configuration: - * - *

- *   x509.keystore.path =
- * 
- * path to Java keystore file
- * 
- *   keystore.password =
- * 
- * password to access the keystore
- * 
- *   ca.cert =
- * 
- * path to certificate file for CA whose client certs to accept.
- * 
- *   autoregister =
- * 
- * "true" if E-Person is created automatically for unknown new users.
- * 
- *   groups =
- * 
- * comma-delimited list of special groups to add user to if authenticated.
- * 
- *   emaildomain =
- * 
- * email address domain (after the 'at' symbol) to match before allowing
- * membership in special groups.
- * 
- * 
- * - * Only one of the "keystore.path" or "ca.cert" - * options is required. If you supply a keystore, then all of the "trusted" - * certificates in the keystore represent CAs whose client certificates will be - * accepted. The ca.cert option only allows a single CA to be - * named. - *

- * You can configure both a keystore and a CA cert, and both will be - * used. - *

- * The autoregister configuration parameter determines what the - * canSelfRegister() method returns. It also allows an EPerson - * record to be created automatically when the presented certificate is - * acceptable but there is no corresponding EPerson. - * - * @author Larry Stone - * @version $Revision$ - */ -public class X509Authentication implements AuthenticationMethod { - - /** - * log4j category - */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(X509Authentication.class); - - /** - * public key of CA to check client certs against. - */ - private static PublicKey caPublicKey = null; - - /** - * key store for CA certs if we use that - */ - private static KeyStore caCertKeyStore = null; - - private static String loginPageTitle = null; - - private static String loginPageURL = null; - - protected AuthenticationService authenticationService = AuthenticateServiceFactory.getInstance() - .getAuthenticationService(); - protected EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); - protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); - protected ConfigurationService configurationService = - DSpaceServicesFactory.getInstance().getConfigurationService(); - - private static final String X509_AUTHENTICATED = "x509.authenticated"; - - - /** - * Initialization: Set caPublicKey and/or keystore. This loads the - * information needed to check if a client cert presented is valid and - * acceptable. - */ - static { - ConfigurationService configurationService = - DSpaceServicesFactory.getInstance().getConfigurationService(); - /* - * allow identification of alternative entry points for certificate - * authentication when selected by the user rather than implicitly. - */ - loginPageTitle = configurationService - .getProperty("authentication-x509.chooser.title.key"); - loginPageURL = configurationService - .getProperty("authentication-x509.chooser.uri"); - - String keystorePath = configurationService - .getProperty("authentication-x509.keystore.path"); - String keystorePassword = configurationService - .getProperty("authentication-x509.keystore.password"); - String caCertPath = configurationService - .getProperty("authentication-x509.ca.cert"); - - // First look for keystore full of trusted certs. - if (keystorePath != null) { - FileInputStream fis = null; - if (keystorePassword == null) { - keystorePassword = ""; - } - try { - KeyStore ks = KeyStore.getInstance("JKS"); - fis = new FileInputStream(keystorePath); - ks.load(fis, keystorePassword.toCharArray()); - caCertKeyStore = ks; - } catch (IOException e) { - log - .error("X509Authentication: Failed to load CA keystore, file=" - + keystorePath + ", error=" + e.toString()); - } catch (GeneralSecurityException e) { - log - .error("X509Authentication: Failed to extract CA keystore, file=" - + keystorePath + ", error=" + e.toString()); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException ioe) { - // ignore - } - } - } - } - - // Second, try getting public key out of CA cert, if that's configured. - if (caCertPath != null) { - InputStream is = null; - FileInputStream fis = null; - try { - fis = new FileInputStream(caCertPath); - is = new BufferedInputStream(fis); - X509Certificate cert = (X509Certificate) CertificateFactory - .getInstance("X.509").generateCertificate(is); - if (cert != null) { - caPublicKey = cert.getPublicKey(); - } - } catch (IOException e) { - log.error("X509Authentication: Failed to load CA cert, file=" - + caCertPath + ", error=" + e.toString()); - } catch (CertificateException e) { - log - .error("X509Authentication: Failed to extract CA cert, file=" - + caCertPath + ", error=" + e.toString()); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException ioe) { - // ignore - } - } - - if (fis != null) { - try { - fis.close(); - } catch (IOException ioe) { - // ignore - } - } - } - } - } - - /** - * Return the email address from certificate, or null if an - * email address cannot be found in the certificate. - *

- * Note that the certificate parsing has only been tested with certificates - * granted by the MIT Certification Authority, and may not work elsewhere. - * - * @param certificate - - * An X509 certificate object - * @return - The email address found in certificate, or null if an email - * address cannot be found in the certificate. - */ - private static String getEmail(X509Certificate certificate) - throws SQLException { - Principal principal = certificate.getSubjectDN(); - - if (principal == null) { - return null; - } - - String dn = principal.getName(); - if (dn == null) { - return null; - } - - StringTokenizer tokenizer = new StringTokenizer(dn, ","); - String token = null; - while (tokenizer.hasMoreTokens()) { - int len = "emailaddress=".length(); - - token = (String) tokenizer.nextToken(); - - if (token.toLowerCase().startsWith("emailaddress=")) { - // Make sure the token actually contains something - if (token.length() <= len) { - return null; - } - - return token.substring(len).toLowerCase(); - } - } - - return null; - } - - /** - * Verify CERTIFICATE against KEY. Return true if and only if CERTIFICATE is - * valid and can be verified against KEY. - * - * @param context The current DSpace context - * @param certificate - - * An X509 certificate object - * @return - True if CERTIFICATE is valid and can be verified against KEY, - * false otherwise. - */ - private static boolean isValid(Context context, X509Certificate certificate) { - if (certificate == null) { - return false; - } - - // This checks that current time is within cert's validity window: - try { - certificate.checkValidity(); - } catch (CertificateException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate is EXPIRED or PREMATURE: " - + e.toString())); - return false; - } - - // Try CA public key, if available. - if (caPublicKey != null) { - try { - certificate.verify(caPublicKey); - return true; - } catch (GeneralSecurityException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate FAILED SIGNATURE check: " - + e.toString())); - } - } - - // Try it with keystore, if available. - if (caCertKeyStore != null) { - try { - Enumeration ke = caCertKeyStore.aliases(); - - while (ke.hasMoreElements()) { - String alias = (String) ke.nextElement(); - if (caCertKeyStore.isCertificateEntry(alias)) { - Certificate ca = caCertKeyStore.getCertificate(alias); - try { - certificate.verify(ca.getPublicKey()); - return true; - } catch (CertificateException ce) { - // ignore - } - } - } - log - .info(LogHelper - .getHeader(context, "authentication", - "Keystore method FAILED SIGNATURE check on client cert.")); - } catch (GeneralSecurityException e) { - log.info(LogHelper.getHeader(context, "authentication", - "X.509 Certificate FAILED SIGNATURE check: " - + e.toString())); - } - - } - return false; - } - - /** - * Predicate, can new user automatically create EPerson. Checks - * configuration value. You'll probably want this to be true to take - * advantage of a Web certificate infrastructure with many more users than - * are already known by DSpace. - * - * @throws SQLException if database error - */ - @Override - public boolean canSelfRegister(Context context, HttpServletRequest request, - String username) throws SQLException { - return configurationService - .getBooleanProperty("authentication-x509.autoregister"); - } - - /** - * Nothing extra to initialize. - * - * @throws SQLException if database error - */ - @Override - public void initEPerson(Context context, HttpServletRequest request, - EPerson eperson) throws SQLException { - } - - /** - * We don't use EPerson password so there is no reason to change it. - * - * @throws SQLException if database error - */ - @Override - public boolean allowSetPassword(Context context, - HttpServletRequest request, String username) throws SQLException { - return false; - } - - /** - * Returns true, this is an implicit method. - */ - @Override - public boolean isImplicit() { - return true; - } - - /** - * Returns a list of group names that the user should be added to upon - * successful authentication, configured in dspace.cfg. - * - * @return List of special groups configured for this authenticator - */ - private List getX509Groups() { - List groupNames = new ArrayList(); - - String[] groups = configurationService - .getArrayProperty("authentication-x509.groups"); - - if (ArrayUtils.isNotEmpty(groups)) { - for (String group : groups) { - groupNames.add(group.trim()); - } - } - - return groupNames; - } - - /** - * Checks for configured email domain required to grant special groups - * membership. If no email domain is configured to verify, special group - * membership is simply granted. - * - * @param request - - * The current request object - * @param email - - * The email address from the x509 certificate - */ - private void setSpecialGroupsFlag(HttpServletRequest request, String email) { - String emailDomain = null; - emailDomain = (String) request - .getAttribute("authentication.x509.emaildomain"); - - HttpSession session = request.getSession(true); - - if (null != emailDomain && !"".equals(emailDomain)) { - if (email.substring(email.length() - emailDomain.length()).equals( - emailDomain)) { - session.setAttribute("x509Auth", Boolean.TRUE); - } - } else { - // No configured email domain to verify. Just flag - // as authenticated so special groups are granted. - session.setAttribute("x509Auth", Boolean.TRUE); - } - } - - /** - * Return special groups configured in dspace.cfg for X509 certificate - * authentication. - * - * @param context context - * @param request object potentially containing the cert - * @return An int array of group IDs - * @throws SQLException if database error - */ - @Override - public List getSpecialGroups(Context context, HttpServletRequest request) - throws SQLException { - if (request == null) { - return Collections.EMPTY_LIST; - } - - Boolean authenticated = false; - HttpSession session = request.getSession(false); - authenticated = (Boolean) session.getAttribute("x509Auth"); - authenticated = (null == authenticated) ? false : authenticated; - - if (authenticated) { - List groupNames = getX509Groups(); - List groups = new ArrayList<>(); - - if (groupNames != null) { - for (String groupName : groupNames) { - if (groupName != null) { - Group group = groupService.findByName(context, groupName); - if (group != null) { - groups.add(group); - } else { - log.warn(LogHelper.getHeader(context, - "configuration_error", "unknown_group=" - + groupName)); - } - } - } - } - - return groups; - } - - return Collections.EMPTY_LIST; - } - - /** - * X509 certificate authentication. The client certificate is obtained from - * the ServletRequest object. - *

    - *
  • If the certificate is valid, and corresponds to an existing EPerson, - * and the user is allowed to login, return success.
  • - *
  • If the user is matched but is not allowed to login, it fails.
  • - *
  • If the certificate is valid, but there is no corresponding EPerson, - * the "authentication.x509.autoregister" configuration - * parameter is checked (via canSelfRegister()) - *
      - *
    • If it's true, a new EPerson record is created for the certificate, - * and the result is success.
    • - *
    • If it's false, return that the user was unknown.
    • - *
    - *
  • - *
- * - * @return One of: SUCCESS, BAD_CREDENTIALS, NO_SUCH_USER, BAD_ARGS - * @throws SQLException if database error - */ - @Override - public int authenticate(Context context, String username, String password, - String realm, HttpServletRequest request) throws SQLException { - // Obtain the certificate from the request, if any - X509Certificate[] certs = null; - if (request != null) { - certs = (X509Certificate[]) request - .getAttribute("javax.servlet.request.X509Certificate"); - } - - if ((certs == null) || (certs.length == 0)) { - return BAD_ARGS; - } else { - // We have a cert -- check it and get username from it. - try { - if (!isValid(context, certs[0])) { - log - .warn(LogHelper - .getHeader(context, "authenticate", - "type=x509certificate, status=BAD_CREDENTIALS (not valid)")); - return BAD_CREDENTIALS; - } - - // And it's valid - try and get an e-person - String email = getEmail(certs[0]); - EPerson eperson = null; - if (email != null) { - eperson = ePersonService.findByEmail(context, email); - } - if (eperson == null) { - // Cert is valid, but no record. - if (email != null - && canSelfRegister(context, request, null)) { - // Register the new user automatically - log.info(LogHelper.getHeader(context, "autoregister", - "from=x.509, email=" + email)); - - // TEMPORARILY turn off authorisation - context.turnOffAuthorisationSystem(); - eperson = ePersonService.create(context); - eperson.setEmail(email); - eperson.setCanLogIn(true); - authenticationService.initEPerson(context, request, - eperson); - ePersonService.update(context, eperson); - context.dispatchEvents(); - context.restoreAuthSystemState(); - context.setCurrentUser(eperson); - request.setAttribute(X509_AUTHENTICATED, true); - setSpecialGroupsFlag(request, email); - return SUCCESS; - } else { - // No auto-registration for valid certs - log - .warn(LogHelper - .getHeader(context, "authenticate", - "type=cert_but_no_record, cannot auto-register")); - return NO_SUCH_USER; - } - } else if (!eperson.canLogIn()) { // make sure this is a login account - log.warn(LogHelper.getHeader(context, "authenticate", - "type=x509certificate, email=" + email - + ", canLogIn=false, rejecting.")); - return BAD_ARGS; - } else { - log.info(LogHelper.getHeader(context, "login", - "type=x509certificate")); - context.setCurrentUser(eperson); - request.setAttribute(X509_AUTHENTICATED, true); - setSpecialGroupsFlag(request, email); - return SUCCESS; - } - } catch (AuthorizeException ce) { - log.warn(LogHelper.getHeader(context, "authorize_exception", - ""), ce); - } - - return BAD_ARGS; - } - } - - /** - * Returns URL of password-login servlet. - * - * @param context DSpace context, will be modified (EPerson set) upon success. - * @param request The HTTP request that started this operation, or null if not - * applicable. - * @param response The HTTP response from the servlet method. - * @return fully-qualified URL - */ - @Override - public String loginPageURL(Context context, HttpServletRequest request, - HttpServletResponse response) { - return loginPageURL; - } - - @Override - public String getName() { - return "x509"; - } - - @Override - public boolean isUsed(final Context context, final HttpServletRequest request) { - if (request != null && - context.getCurrentUser() != null && - request.getAttribute(X509_AUTHENTICATED) != null) { - return true; - } - return false; - } - - @Override - public boolean canChangePassword(Context context, EPerson ePerson, String currentPassword) { - return false; - } -} diff --git a/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java b/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java index e955302ec3d7..105e30d9bdfa 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/service/AuthenticationService.java @@ -29,11 +29,11 @@ * Configuration
* The stack of authentication methods is defined by one property in the DSpace configuration: *
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = a list of method class names
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = a list of method class names
  *     e.g.
- *   plugin.sequence.org.dspace.eperson.AuthenticationMethod = \
- *       org.dspace.eperson.X509Authentication, \
- *       org.dspace.eperson.PasswordAuthentication
+ *   plugin.sequence.org.dspace.authenticate.AuthenticationMethod = \
+ *       org.dspace.authenticate.IPAuthentication, \
+ *       org.dspace.authenticate.PasswordAuthentication
  * 
*

* The "stack" is always traversed in order, with the methods @@ -64,7 +64,7 @@ public interface AuthenticationService { *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. password) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/password not appropriate for this method */ @@ -91,7 +91,7 @@ public int authenticate(Context context, *

Meaning: *
SUCCESS - authenticated OK. *
BAD_CREDENTIALS - user exists, but credentials (e.g. password) don't match - *
CERT_REQUIRED - not allowed to login this way without X.509 cert. + *
CERT_REQUIRED - not allowed to login this way without a cert. *
NO_SUCH_USER - user not found using this method. *
BAD_ARGS - user/password not appropriate for this method */ diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java b/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java index 343ddcccfa39..53451e07a9ec 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonCLITool.java @@ -50,7 +50,7 @@ public class EPersonCLITool { private static final Option OPT_PHONE = new Option("t", "telephone", true, "telephone number, empty for none"); private static final Option OPT_LANGUAGE = new Option("l", "language", true, "the person's preferred language"); private static final Option OPT_REQUIRE_CERTIFICATE = new Option("c", "requireCertificate", true, - "if 'true', an X.509 certificate will be " + + "if 'true', a certificate will be " + "required for login"); private static final Option OPT_CAN_LOGIN = new Option("C", "canLogIn", true, "'true' if the user can log in"); diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 55ba2e0bffb5..0fcf0022bd13 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1641,7 +1641,6 @@ include = ${module_dir}/authentication-ldap.cfg include = ${module_dir}/authentication-oidc.cfg include = ${module_dir}/authentication-password.cfg include = ${module_dir}/authentication-shibboleth.cfg -include = ${module_dir}/authentication-x509.cfg include = ${module_dir}/authority.cfg include = ${module_dir}/bulkedit.cfg include = ${module_dir}/citation-page.cfg diff --git a/dspace/config/local.cfg.EXAMPLE b/dspace/config/local.cfg.EXAMPLE index 7a565823e8bf..b35e7d20eefc 100644 --- a/dspace/config/local.cfg.EXAMPLE +++ b/dspace/config/local.cfg.EXAMPLE @@ -210,9 +210,6 @@ db.schema = public # ORCID certificate authentication. # plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.OrcidAuthentication -# X.509 certificate authentication. See authentication-x509.cfg for default configuration. -#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.X509Authentication - # Authentication by Password (encrypted in DSpace's database). See authentication-password.cfg for default configuration. # Enabled by default in authentication.cfg #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.PasswordAuthentication diff --git a/dspace/config/modules/authentication-x509.cfg b/dspace/config/modules/authentication-x509.cfg deleted file mode 100644 index d3f05c7d17d5..000000000000 --- a/dspace/config/modules/authentication-x509.cfg +++ /dev/null @@ -1,23 +0,0 @@ -#---------------------------------------------------------------# -#------X.509 CERTIFICATE AUTHENTICATION CONFIGURATIONS----------# -#---------------------------------------------------------------# -# Configuration properties used by the X.509 Certificate # -# Authentication plugin, when it is enabled. # -#---------------------------------------------------------------# - -## method 1, using keystore -#authentication-x509.keystore.path = /tomcat/conf/keystore -#authentication-x509.keystore.password = changeit - -## method 2, using CA certificate -#authentication-x509.ca.cert = ${dspace.dir}/config/MyClientCA.pem - -## Create e-persons for unknown names in valid certificates? -#authentication-x509.autoregister = true - -## Allow Certificate auth to show as a choice in chooser -# Use Messages.properties key for title -#authentication-x509.chooser.title.key=org.dspace.eperson.X509Authentication.title -# -# Identify the location of the Certificate Login Servlet. -#authentication-x509.chooser.uri=/certificate-login diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index 568f871e3cd7..41c28df1d7c9 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -21,9 +21,6 @@ # * IP Address Authentication # Plugin class: org.dspace.authenticate.IPAuthentication # Configuration file: authentication-ip.cfg -# * X.509 Certificate Authentication -# Plugin class: org.dspace.authenticate.X509Authentication -# Configuration file: authentication-x509.cfg # * ORCID certificate authentication. # Plugin class: org.dspace.authenticate.OrcidAuthentication # Configuration file: orcid.cfg @@ -49,9 +46,6 @@ # Shibboleth authentication/authorization. See authentication-shibboleth.cfg for default configuration. #plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.ShibAuthentication -# X.509 certificate authentication. See authentication-x509.cfg for default configuration. -#plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.X509Authentication - # ORCID certificate authentication. # plugin.sequence.org.dspace.authenticate.AuthenticationMethod = org.dspace.authenticate.OrcidAuthentication From b2f862500a56fa585dd16509bffb47401c2f2419 Mon Sep 17 00:00:00 2001 From: Zahraa Chreim Date: Wed, 19 Nov 2025 08:42:33 +0200 Subject: [PATCH 426/701] 135699: Fix NPE in Bitstream deletion by reversing bitstream comparison --- .../src/main/java/org/dspace/content/BitstreamServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index 0c5d12992435..c19b87089e64 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -295,7 +295,7 @@ public void delete(Context context, Bitstream bitstream) throws SQLException, Au // a deleted bitstream List requestItems = requestItemService.findAll(context); for (RequestItem requestItem : requestItems) { - if (requestItem.getBitstream().equals(bitstream)) { + if (bitstream.equals(requestItem.getBitstream())) { requestItemService.delete(context, requestItem); } } From 13b0bbaaa92d82e69b76073588ebd090a8211fe8 Mon Sep 17 00:00:00 2001 From: Zahraa Chreim Date: Wed, 19 Nov 2025 08:42:33 +0200 Subject: [PATCH 427/701] 135699: Fix NPE in Bitstream deletion by reversing bitstream comparison (cherry picked from commit b2f862500a56fa585dd16509bffb47401c2f2419) --- .../src/main/java/org/dspace/content/BitstreamServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index 7253761fd902..3a2670395dbe 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -295,7 +295,7 @@ public void delete(Context context, Bitstream bitstream) throws SQLException, Au // a deleted bitstream List requestItems = requestItemService.findAll(context); for (RequestItem requestItem : requestItems) { - if (requestItem.getBitstream().equals(bitstream)) { + if (bitstream.equals(requestItem.getBitstream())) { requestItemService.delete(context, requestItem); } } From 5147dea804491e52a9cd73d79f308bb7026a6633 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Fri, 26 Sep 2025 10:29:43 -0400 Subject: [PATCH 428/701] Use canonical path for bitstream path/assetstore path comparisons Replaced `baseDir.getAbsolutePath()` with `baseDir.getCanonicalPath()` in the bitstream path check. This is necessary because the "normzalizedPath" is generated via the "getCanonicalFile" method. This method follows symbolic links, unlike the "getAbsolutePath" method, which does not. Therefore, when comparing the "normalizedPath" to the "baseDir", it is necessary to use "getCanonicalPath" so that any symbolic links are resolved in the same way. This issue was uncovered by running the "org.dspace.storage.bitstore.BitstreamStorageServiceImplIT" integration tests on macOS, in which a temporary file asset store is used. On macOS the temporary files are stored in "/var" which is a symbolic link to "/private/var". (cherry picked from commit 8f8cb52658954234c05e1ea71e43375f080b7d0f) --- .../java/org/dspace/storage/bitstore/DSBitStoreService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java index fd4e23b82581..a08af8104ccd 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java @@ -256,11 +256,11 @@ protected File getFile(Bitstream bitstream) throws IOException { Path normalizedPath = bitstreamFile.toPath().normalize(); String[] allowedAssetstoreRoots = DSpaceServicesFactory.getInstance().getConfigurationService() .getArrayProperty("assetstore.allowed.roots", new String[]{}); - if (!normalizedPath.startsWith(baseDir.getAbsolutePath()) + if (!normalizedPath.startsWith(baseDir.getCanonicalPath()) && !StringUtils.startsWithAny(normalizedPath.toString(), allowedAssetstoreRoots)) { log.error("Bitstream path outside of assetstore root requested:" + "bitstream={}, path={}, assetstore={}", - bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); + bitstream.getID(), normalizedPath, baseDir.getCanonicalPath()); throw new IOException("Illegal bitstream path constructed"); } return bitstreamFile; From 3026942d99914f93de90059197625adad102ea21 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Fri, 26 Sep 2025 10:29:43 -0400 Subject: [PATCH 429/701] Use canonical path for bitstream path/assetstore path comparisons Replaced `baseDir.getAbsolutePath()` with `baseDir.getCanonicalPath()` in the bitstream path check. This is necessary because the "normzalizedPath" is generated via the "getCanonicalFile" method. This method follows symbolic links, unlike the "getAbsolutePath" method, which does not. Therefore, when comparing the "normalizedPath" to the "baseDir", it is necessary to use "getCanonicalPath" so that any symbolic links are resolved in the same way. This issue was uncovered by running the "org.dspace.storage.bitstore.BitstreamStorageServiceImplIT" integration tests on macOS, in which a temporary file asset store is used. On macOS the temporary files are stored in "/var" which is a symbolic link to "/private/var". (cherry picked from commit 8f8cb52658954234c05e1ea71e43375f080b7d0f) --- .../java/org/dspace/storage/bitstore/DSBitStoreService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java index fd4e23b82581..a08af8104ccd 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/DSBitStoreService.java @@ -256,11 +256,11 @@ protected File getFile(Bitstream bitstream) throws IOException { Path normalizedPath = bitstreamFile.toPath().normalize(); String[] allowedAssetstoreRoots = DSpaceServicesFactory.getInstance().getConfigurationService() .getArrayProperty("assetstore.allowed.roots", new String[]{}); - if (!normalizedPath.startsWith(baseDir.getAbsolutePath()) + if (!normalizedPath.startsWith(baseDir.getCanonicalPath()) && !StringUtils.startsWithAny(normalizedPath.toString(), allowedAssetstoreRoots)) { log.error("Bitstream path outside of assetstore root requested:" + "bitstream={}, path={}, assetstore={}", - bitstream.getID(), normalizedPath, baseDir.getAbsolutePath()); + bitstream.getID(), normalizedPath, baseDir.getCanonicalPath()); throw new IOException("Illegal bitstream path constructed"); } return bitstreamFile; From 8e1b94f6834087a8525d0d2ced1f71818e5767a4 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:50:04 -0300 Subject: [PATCH 430/701] fix(#10176): Fix inverted parameters for Shibboleth metadata update (cherry picked from commit 779e929d51758acd6b03780b2b562877e53b3ce2) --- .../main/java/org/dspace/authenticate/ShibAuthentication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java index ea6dd0347ee7..3012c907845f 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java @@ -870,7 +870,7 @@ protected void updateEPerson(Context context, HttpServletRequest request, EPerso String[] nameParts = MetadataFieldName.parse(field); ePersonService.setMetadataSingleValue(context, eperson, - nameParts[0], nameParts[1], nameParts[2], value, null); + nameParts[0], nameParts[1], nameParts[2], null, value); log.debug("Updated the eperson's '{}' metadata using header: '{}' = '{}'.", field, header, value); } From 1cb11cbc23f24601f9bf4228aa40670de9ec7fde Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:14:07 -0300 Subject: [PATCH 431/701] test(DSpace#10176): Add unit test for Shibboleth metadata update regression This class contains unit tests for the ShibAuthentication functionality, verifying metadata updates and initialization behavior. (cherry picked from commit 748a11fa20208017a78b1d97a0d9095a15064c83) --- .../authenticate/ShibAuthenticationTest.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java diff --git a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java new file mode 100644 index 000000000000..0f9daddf57a8 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java @@ -0,0 +1,128 @@ +package org.dspace.authenticate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import jakarta.servlet.http.HttpServletRequest; +import org.dspace.AbstractUnitTest; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchema; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.service.EPersonService; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +/** + * Unit tests for ShibAuthentication + */ +public class ShibAuthenticationTest extends AbstractUnitTest { + + private ShibAuthentication shibAuthentication; + private EPersonService ePersonService; + private ConfigurationService configurationService; + + @Before + public void setup() { + shibAuthentication = new ShibAuthentication(); + ePersonService = mock(EPersonService.class); + shibAuthentication.ePersonService = ePersonService; + configurationService = mock(ConfigurationService.class); + shibAuthentication.configurationService = configurationService; + when(configurationService.getProperty("authentication-shibboleth.netid-header")).thenReturn("SHIB-NETID"); + when(configurationService.getProperty("authentication-shibboleth.email-header")).thenReturn("SHIB-MAIL"); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[]{"SHIB-telephone => eperson.phone"}); + when(configurationService.getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true)).thenReturn(true); + MetadataFieldService metadataFieldService = mock(MetadataFieldService.class); + shibAuthentication.metadataFieldService = metadataFieldService; + + try { + when(metadataFieldService.findByElement(any(Context.class), any(String.class), any(String.class), any())) + .thenReturn(mock(MetadataField.class)); + } catch (Exception e) { + // ignore checked exceptions from mock + } + } + + @Test + public void testPhoneMetadataUpdateOrder() throws Exception { + Context context = mock(Context.class); + HttpServletRequest request = mock(HttpServletRequest.class); + EPerson eperson = mock(EPerson.class); + when(request.getAttribute("SHIB-NETID")).thenReturn("test-user"); + when(request.getAttribute("SHIB-MAIL")).thenReturn("test@example.com"); + String phoneValue = "555-1234"; + when(request.getAttribute("SHIB-telephone")).thenReturn(phoneValue); + shibAuthentication.initialize(context); + assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap.containsKey("SHIB-telephone")); + shibAuthentication.updateEPerson(context, request, eperson); + ArgumentCaptor languageCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); + + verify(ePersonService, times(1)).setMetadataSingleValue( + any(Context.class), + eq(eperson), + eq("eperson"), + eq("phone"), + isNull(), + languageCaptor.capture(), + valueCaptor.capture() + ); + + assertNull("The language argument should be NULL.", languageCaptor.getValue()); + assertEquals("The value argument should be the phone number.", phoneValue, valueCaptor.getValue()); + } + + @Test + public void testInitializeLoadsMultipleMappings() throws Exception { + Context context = mock(Context.class); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[]{ + "SHIB-telephone => eperson.phone", + "SHIB-dept => eperson.department" + }); + shibAuthentication.initialize(context); + + assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap.containsKey("SHIB-telephone")); + assertTrue("metadataHeaderMap should contain SHIB-dept", shibAuthentication.metadataHeaderMap.containsKey("SHIB-dept")); + } + + @Test + public void testNoMetadataMappingNoUpdate() throws Exception { + Context context = mock(Context.class); + HttpServletRequest request = mock(HttpServletRequest.class); + EPerson eperson = mock(EPerson.class); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[0]); + shibAuthentication.initialize(context); + shibAuthentication.updateEPerson(context, request, eperson); + + verify(ePersonService, times(0)).setMetadataSingleValue( + any(Context.class), + any(EPerson.class), + anyString(), + anyString(), + any(), + any(), + any() + ); + } +} From 915a6a4448a4e494754bb4382d6fad89889a36a2 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:39:02 -0300 Subject: [PATCH 432/701] Added missing license header (cherry picked from commit f742549f72c71aebcc71a54fa35df643f617d741) --- .../org/dspace/authenticate/ShibAuthenticationTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java index 0f9daddf57a8..d3fe53f2b5bb 100644 --- a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java +++ b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java @@ -1,3 +1,10 @@ +/** + * 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.authenticate; import static org.junit.Assert.assertEquals; From 8f0a4c9a1ceb73334b58e70552d3d78f087dd990 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:55:16 -0300 Subject: [PATCH 433/701] Fixed checkstyle errors (cherry picked from commit deccd07d7d2821dc593e43cc0d2697d263a1905c) --- .../authenticate/ShibAuthenticationTest.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java index d3fe53f2b5bb..e0f0fc57a9ea 100644 --- a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java +++ b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java @@ -8,25 +8,22 @@ package org.dspace.authenticate; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Collections; import jakarta.servlet.http.HttpServletRequest; import org.dspace.AbstractUnitTest; -import org.dspace.content.service.MetadataFieldService; import org.dspace.content.MetadataField; -import org.dspace.content.MetadataSchema; +import org.dspace.content.service.MetadataFieldService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; @@ -55,7 +52,8 @@ public void setup() { when(configurationService.getProperty("authentication-shibboleth.email-header")).thenReturn("SHIB-MAIL"); when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) .thenReturn(new String[]{"SHIB-telephone => eperson.phone"}); - when(configurationService.getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true)).thenReturn(true); + when(configurationService.getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true)) + .thenReturn(true); MetadataFieldService metadataFieldService = mock(MetadataFieldService.class); shibAuthentication.metadataFieldService = metadataFieldService; @@ -78,7 +76,8 @@ public void testPhoneMetadataUpdateOrder() throws Exception { when(request.getAttribute("SHIB-telephone")).thenReturn(phoneValue); shibAuthentication.initialize(context); assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); - assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap.containsKey("SHIB-telephone")); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-telephone")); shibAuthentication.updateEPerson(context, request, eperson); ArgumentCaptor languageCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); @@ -92,7 +91,7 @@ public void testPhoneMetadataUpdateOrder() throws Exception { languageCaptor.capture(), valueCaptor.capture() ); - + assertNull("The language argument should be NULL.", languageCaptor.getValue()); assertEquals("The value argument should be the phone number.", phoneValue, valueCaptor.getValue()); } @@ -108,8 +107,10 @@ public void testInitializeLoadsMultipleMappings() throws Exception { shibAuthentication.initialize(context); assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); - assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap.containsKey("SHIB-telephone")); - assertTrue("metadataHeaderMap should contain SHIB-dept", shibAuthentication.metadataHeaderMap.containsKey("SHIB-dept")); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-telephone")); + assertTrue("metadataHeaderMap should contain SHIB-dept", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-dept")); } @Test From 128392eb9112998674ae001db9c9d024c29a337f Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Fri, 14 Nov 2025 18:50:04 -0300 Subject: [PATCH 434/701] fix(#10176): Fix inverted parameters for Shibboleth metadata update (cherry picked from commit 779e929d51758acd6b03780b2b562877e53b3ce2) --- .../main/java/org/dspace/authenticate/ShibAuthentication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java index b5eed7e764b0..13a5ae6d0dfd 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/ShibAuthentication.java @@ -870,7 +870,7 @@ protected void updateEPerson(Context context, HttpServletRequest request, EPerso String[] nameParts = MetadataFieldName.parse(field); ePersonService.setMetadataSingleValue(context, eperson, - nameParts[0], nameParts[1], nameParts[2], value, null); + nameParts[0], nameParts[1], nameParts[2], null, value); log.debug("Updated the eperson's '{}' metadata using header: '{}' = '{}'.", field, header, value); } From 968d8bf9cd5ca2eda5cbf91c8a9fb226e8037042 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:14:07 -0300 Subject: [PATCH 435/701] test(DSpace#10176): Add unit test for Shibboleth metadata update regression This class contains unit tests for the ShibAuthentication functionality, verifying metadata updates and initialization behavior. (cherry picked from commit 748a11fa20208017a78b1d97a0d9095a15064c83) --- .../authenticate/ShibAuthenticationTest.java | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java diff --git a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java new file mode 100644 index 000000000000..0f9daddf57a8 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java @@ -0,0 +1,128 @@ +package org.dspace.authenticate; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import jakarta.servlet.http.HttpServletRequest; +import org.dspace.AbstractUnitTest; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchema; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.service.EPersonService; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +/** + * Unit tests for ShibAuthentication + */ +public class ShibAuthenticationTest extends AbstractUnitTest { + + private ShibAuthentication shibAuthentication; + private EPersonService ePersonService; + private ConfigurationService configurationService; + + @Before + public void setup() { + shibAuthentication = new ShibAuthentication(); + ePersonService = mock(EPersonService.class); + shibAuthentication.ePersonService = ePersonService; + configurationService = mock(ConfigurationService.class); + shibAuthentication.configurationService = configurationService; + when(configurationService.getProperty("authentication-shibboleth.netid-header")).thenReturn("SHIB-NETID"); + when(configurationService.getProperty("authentication-shibboleth.email-header")).thenReturn("SHIB-MAIL"); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[]{"SHIB-telephone => eperson.phone"}); + when(configurationService.getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true)).thenReturn(true); + MetadataFieldService metadataFieldService = mock(MetadataFieldService.class); + shibAuthentication.metadataFieldService = metadataFieldService; + + try { + when(metadataFieldService.findByElement(any(Context.class), any(String.class), any(String.class), any())) + .thenReturn(mock(MetadataField.class)); + } catch (Exception e) { + // ignore checked exceptions from mock + } + } + + @Test + public void testPhoneMetadataUpdateOrder() throws Exception { + Context context = mock(Context.class); + HttpServletRequest request = mock(HttpServletRequest.class); + EPerson eperson = mock(EPerson.class); + when(request.getAttribute("SHIB-NETID")).thenReturn("test-user"); + when(request.getAttribute("SHIB-MAIL")).thenReturn("test@example.com"); + String phoneValue = "555-1234"; + when(request.getAttribute("SHIB-telephone")).thenReturn(phoneValue); + shibAuthentication.initialize(context); + assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap.containsKey("SHIB-telephone")); + shibAuthentication.updateEPerson(context, request, eperson); + ArgumentCaptor languageCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); + + verify(ePersonService, times(1)).setMetadataSingleValue( + any(Context.class), + eq(eperson), + eq("eperson"), + eq("phone"), + isNull(), + languageCaptor.capture(), + valueCaptor.capture() + ); + + assertNull("The language argument should be NULL.", languageCaptor.getValue()); + assertEquals("The value argument should be the phone number.", phoneValue, valueCaptor.getValue()); + } + + @Test + public void testInitializeLoadsMultipleMappings() throws Exception { + Context context = mock(Context.class); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[]{ + "SHIB-telephone => eperson.phone", + "SHIB-dept => eperson.department" + }); + shibAuthentication.initialize(context); + + assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap.containsKey("SHIB-telephone")); + assertTrue("metadataHeaderMap should contain SHIB-dept", shibAuthentication.metadataHeaderMap.containsKey("SHIB-dept")); + } + + @Test + public void testNoMetadataMappingNoUpdate() throws Exception { + Context context = mock(Context.class); + HttpServletRequest request = mock(HttpServletRequest.class); + EPerson eperson = mock(EPerson.class); + when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) + .thenReturn(new String[0]); + shibAuthentication.initialize(context); + shibAuthentication.updateEPerson(context, request, eperson); + + verify(ePersonService, times(0)).setMetadataSingleValue( + any(Context.class), + any(EPerson.class), + anyString(), + anyString(), + any(), + any(), + any() + ); + } +} From 159b1f095e0eede5460377bc24a69a8cac17b8a3 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Wed, 19 Nov 2025 09:39:02 -0300 Subject: [PATCH 436/701] Added missing license header (cherry picked from commit f742549f72c71aebcc71a54fa35df643f617d741) --- .../org/dspace/authenticate/ShibAuthenticationTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java index 0f9daddf57a8..d3fe53f2b5bb 100644 --- a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java +++ b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java @@ -1,3 +1,10 @@ +/** + * 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.authenticate; import static org.junit.Assert.assertEquals; From 5aecee7a7f613a0647e181751e459abd2fa861e1 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Wed, 19 Nov 2025 10:55:16 -0300 Subject: [PATCH 437/701] Fixed checkstyle errors (cherry picked from commit deccd07d7d2821dc593e43cc0d2697d263a1905c) --- .../authenticate/ShibAuthenticationTest.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java index d3fe53f2b5bb..e0f0fc57a9ea 100644 --- a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java +++ b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java @@ -8,25 +8,22 @@ package org.dspace.authenticate; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.util.Collections; import jakarta.servlet.http.HttpServletRequest; import org.dspace.AbstractUnitTest; -import org.dspace.content.service.MetadataFieldService; import org.dspace.content.MetadataField; -import org.dspace.content.MetadataSchema; +import org.dspace.content.service.MetadataFieldService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; @@ -55,7 +52,8 @@ public void setup() { when(configurationService.getProperty("authentication-shibboleth.email-header")).thenReturn("SHIB-MAIL"); when(configurationService.getArrayProperty("authentication-shibboleth.eperson.metadata")) .thenReturn(new String[]{"SHIB-telephone => eperson.phone"}); - when(configurationService.getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true)).thenReturn(true); + when(configurationService.getBooleanProperty("authentication-shibboleth.eperson.metadata.autocreate", true)) + .thenReturn(true); MetadataFieldService metadataFieldService = mock(MetadataFieldService.class); shibAuthentication.metadataFieldService = metadataFieldService; @@ -78,7 +76,8 @@ public void testPhoneMetadataUpdateOrder() throws Exception { when(request.getAttribute("SHIB-telephone")).thenReturn(phoneValue); shibAuthentication.initialize(context); assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); - assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap.containsKey("SHIB-telephone")); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-telephone")); shibAuthentication.updateEPerson(context, request, eperson); ArgumentCaptor languageCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor valueCaptor = ArgumentCaptor.forClass(String.class); @@ -92,7 +91,7 @@ public void testPhoneMetadataUpdateOrder() throws Exception { languageCaptor.capture(), valueCaptor.capture() ); - + assertNull("The language argument should be NULL.", languageCaptor.getValue()); assertEquals("The value argument should be the phone number.", phoneValue, valueCaptor.getValue()); } @@ -108,8 +107,10 @@ public void testInitializeLoadsMultipleMappings() throws Exception { shibAuthentication.initialize(context); assertNotNull("metadataHeaderMap should be initialized", shibAuthentication.metadataHeaderMap); - assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap.containsKey("SHIB-telephone")); - assertTrue("metadataHeaderMap should contain SHIB-dept", shibAuthentication.metadataHeaderMap.containsKey("SHIB-dept")); + assertTrue("metadataHeaderMap should contain SHIB-telephone", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-telephone")); + assertTrue("metadataHeaderMap should contain SHIB-dept", shibAuthentication.metadataHeaderMap + .containsKey("SHIB-dept")); } @Test From b6b7e9d1bfa6361dd87bb1ffd8041b4efe31a2ad Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 19 Nov 2025 13:23:40 -0600 Subject: [PATCH 438/701] Replace with javax version which is required in DSpace 7 --- .../java/org/dspace/authenticate/ShibAuthenticationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java index e0f0fc57a9ea..11005608f329 100644 --- a/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java +++ b/dspace-api/src/test/java/org/dspace/authenticate/ShibAuthenticationTest.java @@ -20,7 +20,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import jakarta.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequest; + import org.dspace.AbstractUnitTest; import org.dspace.content.MetadataField; import org.dspace.content.service.MetadataFieldService; From 20b5152a1f369f61a2e18ce6686801b7fc7ee163 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:15:54 +0000 Subject: [PATCH 439/701] build(deps-dev): bump org.scala-lang:scala-library Bumps [org.scala-lang:scala-library](https://github.com/scala/scala) from 2.13.17 to 2.13.18. - [Release notes](https://github.com/scala/scala/releases) - [Commits](https://github.com/scala/scala/compare/v2.13.17...v2.13.18) --- updated-dependencies: - dependency-name: org.scala-lang:scala-library dependency-version: 2.13.18 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 042917259750..196754215b1a 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -933,7 +933,7 @@ org.scala-lang scala-library - 2.13.17 + 2.13.18 test From d36a026f076eb44de7b5aa0f686fa84952eb1ac1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:16:32 +0000 Subject: [PATCH 440/701] build(deps-dev): bump org.testcontainers:testcontainers-localstack Bumps [org.testcontainers:testcontainers-localstack](https://github.com/testcontainers/testcontainers-java) from 2.0.1 to 2.0.2. - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/2.0.1...2.0.2) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers-localstack dependency-version: 2.0.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 042917259750..c86267f09ef9 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -857,7 +857,7 @@ org.testcontainers testcontainers-localstack - 2.0.1 + 2.0.2 test From 3d8ff72cbe7e49e40cd5d6b8904bda7fff9d4713 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:31:06 +0000 Subject: [PATCH 441/701] build(deps): bump org.checkerframework:checker-qual Bumps [org.checkerframework:checker-qual](https://github.com/typetools/checker-framework) from 3.51.1 to 3.52.0. - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-3.51.1...checker-framework-3.52.0) --- updated-dependencies: - dependency-name: org.checkerframework:checker-qual dependency-version: 3.52.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 61b167bd6a8d..f63927b34b15 100644 --- a/pom.xml +++ b/pom.xml @@ -1348,7 +1348,7 @@ org.checkerframework checker-qual - 3.51.1 + 3.52.0 17 - 6.2.12 - 3.5.7 - 6.5.6 + 6.2.14 + 3.5.8 + 6.5.7 6.4.10.Final 8.0.3.Final 42.7.8 From 172958e43304b6e3bc0f626c31e5d86614e83b49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:31:06 +0000 Subject: [PATCH 451/701] build(deps): bump the apache-commons group across 1 directory with 6 updates Bumps the apache-commons group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [commons-cli:commons-cli](https://github.com/apache/commons-cli) | `1.10.0` | `1.11.0` | | [commons-codec:commons-codec](https://github.com/apache/commons-codec) | `1.19.0` | `1.20.0` | | org.apache.commons:commons-configuration2 | `2.12.0` | `2.13.0` | | [commons-io:commons-io](https://github.com/apache/commons-io) | `2.20.0` | `2.21.0` | | org.apache.commons:commons-lang3 | `3.19.0` | `3.20.0` | | [commons-validator:commons-validator](https://github.com/apache/commons-validator) | `1.10.0` | `1.10.1` | Updates `commons-cli:commons-cli` from 1.10.0 to 1.11.0 - [Changelog](https://github.com/apache/commons-cli/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-cli/compare/rel/commons-cli-1.10.0...rel/commons-cli-1.11.0) Updates `commons-codec:commons-codec` from 1.19.0 to 1.20.0 - [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.19.0...rel/commons-codec-1.20.0) Updates `org.apache.commons:commons-configuration2` from 2.12.0 to 2.13.0 Updates `commons-io:commons-io` from 2.20.0 to 2.21.0 - [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.20.0...rel/commons-io-2.21.0) Updates `org.apache.commons:commons-lang3` from 3.19.0 to 3.20.0 Updates `commons-validator:commons-validator` from 1.10.0 to 1.10.1 - [Changelog](https://github.com/apache/commons-validator/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-validator/compare/rel/commons-validator-1.10.0...rel/commons-validator-1.10.1) --- updated-dependencies: - dependency-name: commons-cli:commons-cli dependency-version: 1.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-codec:commons-codec dependency-version: 1.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-configuration2 dependency-version: 2.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-io:commons-io dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-lang3 dependency-version: 3.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-validator:commons-validator dependency-version: 1.10.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 2b8075d9669f..b0c3cd447fef 100644 --- a/pom.xml +++ b/pom.xml @@ -1473,12 +1473,12 @@ commons-cli commons-cli - 1.10.0 + 1.11.0 commons-codec commons-codec - 1.19.0 + 1.20.0 org.apache.commons @@ -1488,7 +1488,7 @@ org.apache.commons commons-configuration2 - 2.12.0 + 2.13.0 org.apache.commons @@ -1498,12 +1498,12 @@ commons-io commons-io - 2.20.0 + 2.21.0 org.apache.commons commons-lang3 - 3.19.0 + 3.20.0 @@ -1530,7 +1530,7 @@ commons-validator commons-validator - 1.10.0 + 1.10.1 joda-time From 94fc89c23fac156de5233870609035899c8d00ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:48:21 +0000 Subject: [PATCH 452/701] build(deps): bump the build-tools group across 1 directory with 9 updates Bumps the build-tools group with 9 updates in the / directory: | Package | From | To | | --- | --- | --- | | [com.github.spotbugs:spotbugs](https://github.com/spotbugs/spotbugs) | `4.9.6` | `4.9.8` | | [org.apache.maven.plugins:maven-jar-plugin](https://github.com/apache/maven-jar-plugin) | `3.4.2` | `3.5.0` | | [org.apache.maven.plugins:maven-war-plugin](https://github.com/apache/maven-war-plugin) | `3.4.0` | `3.5.0` | | [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) | `4.9.6.0` | `4.9.8.2` | | [org.apache.maven.plugins:maven-assembly-plugin](https://github.com/apache/maven-assembly-plugin) | `3.7.1` | `3.8.0` | | [org.apache.maven.plugins:maven-resources-plugin](https://github.com/apache/maven-resources-plugin) | `3.3.1` | `3.4.0` | | [org.apache.maven.plugins:maven-source-plugin](https://github.com/apache/maven-source-plugin) | `3.3.1` | `3.4.0` | | [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) | `0.8.13` | `0.8.14` | | [org.codehaus.mojo:xml-maven-plugin](https://github.com/mojohaus/xml-maven-plugin) | `1.1.0` | `1.2.0` | Updates `com.github.spotbugs:spotbugs` from 4.9.6 to 4.9.8 - [Release notes](https://github.com/spotbugs/spotbugs/releases) - [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotbugs/spotbugs/compare/4.9.6...4.9.8) Updates `org.apache.maven.plugins:maven-jar-plugin` from 3.4.2 to 3.5.0 - [Release notes](https://github.com/apache/maven-jar-plugin/releases) - [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.4.2...maven-jar-plugin-3.5.0) Updates `org.apache.maven.plugins:maven-war-plugin` from 3.4.0 to 3.5.0 - [Release notes](https://github.com/apache/maven-war-plugin/releases) - [Commits](https://github.com/apache/maven-war-plugin/compare/maven-war-plugin-3.4.0...maven-war-plugin-3.5.0) Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.9.6.0 to 4.9.8.2 - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.6.0...spotbugs-maven-plugin-4.9.8.2) Updates `org.apache.maven.plugins:maven-assembly-plugin` from 3.7.1 to 3.8.0 - [Release notes](https://github.com/apache/maven-assembly-plugin/releases) - [Commits](https://github.com/apache/maven-assembly-plugin/compare/maven-assembly-plugin-3.7.1...v3.8.0) Updates `org.apache.maven.plugins:maven-resources-plugin` from 3.3.1 to 3.4.0 - [Release notes](https://github.com/apache/maven-resources-plugin/releases) - [Commits](https://github.com/apache/maven-resources-plugin/compare/maven-resources-plugin-3.3.1...v3.4.0) Updates `org.apache.maven.plugins:maven-source-plugin` from 3.3.1 to 3.4.0 - [Release notes](https://github.com/apache/maven-source-plugin/releases) - [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-3.3.1...maven-source-plugin-3.4.0) Updates `org.jacoco:jacoco-maven-plugin` from 0.8.13 to 0.8.14 - [Release notes](https://github.com/jacoco/jacoco/releases) - [Commits](https://github.com/jacoco/jacoco/compare/v0.8.13...v0.8.14) Updates `org.codehaus.mojo:xml-maven-plugin` from 1.1.0 to 1.2.0 - [Release notes](https://github.com/mojohaus/xml-maven-plugin/releases) - [Commits](https://github.com/mojohaus/xml-maven-plugin/compare/1.1.0...1.2.0) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs dependency-version: 4.9.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-jar-plugin dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-war-plugin dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.8.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-assembly-plugin dependency-version: 3.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-resources-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-source-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.jacoco:jacoco-maven-plugin dependency-version: 0.8.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.codehaus.mojo:xml-maven-plugin dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index 2b8075d9669f..bed7191044ba 100644 --- a/pom.xml +++ b/pom.xml @@ -174,7 +174,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.4.2 + 3.5.0 @@ -188,7 +188,7 @@ org.apache.maven.plugins maven-war-plugin - 3.4.0 + 3.5.1 false @@ -295,7 +295,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.6.0 + 4.9.8.2 Max Low @@ -305,7 +305,7 @@ com.github.spotbugs spotbugs - 4.9.6 + 4.9.8 @@ -335,7 +335,7 @@ maven-assembly-plugin - 3.7.1 + 3.8.0 org.apache.maven.plugins @@ -345,7 +345,7 @@ org.apache.maven.plugins maven-resources-plugin - 3.3.1 + 3.4.0 @@ -373,7 +373,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.1 + 3.4.0 @@ -392,7 +392,7 @@ org.jacoco jacoco-maven-plugin - 0.8.13 + 0.8.14 @@ -472,7 +472,7 @@ org.codehaus.mojo xml-maven-plugin - 1.1.0 + 1.2.0 validate-ALL-xml-and-xsl From a548649bd21105598fec36376a0a3c4d0844985b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:50:05 +0000 Subject: [PATCH 453/701] build(deps): bump the apache-commons group across 1 directory with 6 updates Bumps the apache-commons group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [commons-cli:commons-cli](https://github.com/apache/commons-cli) | `1.10.0` | `1.11.0` | | [commons-codec:commons-codec](https://github.com/apache/commons-codec) | `1.19.0` | `1.20.0` | | org.apache.commons:commons-configuration2 | `2.12.0` | `2.13.0` | | [commons-io:commons-io](https://github.com/apache/commons-io) | `2.20.0` | `2.21.0` | | org.apache.commons:commons-lang3 | `3.19.0` | `3.20.0` | | [commons-validator:commons-validator](https://github.com/apache/commons-validator) | `1.10.0` | `1.10.1` | Updates `commons-cli:commons-cli` from 1.10.0 to 1.11.0 - [Changelog](https://github.com/apache/commons-cli/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-cli/compare/rel/commons-cli-1.10.0...rel/commons-cli-1.11.0) Updates `commons-codec:commons-codec` from 1.19.0 to 1.20.0 - [Changelog](https://github.com/apache/commons-codec/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-codec/compare/rel/commons-codec-1.19.0...rel/commons-codec-1.20.0) Updates `org.apache.commons:commons-configuration2` from 2.12.0 to 2.13.0 Updates `commons-io:commons-io` from 2.20.0 to 2.21.0 - [Changelog](https://github.com/apache/commons-io/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-io/compare/rel/commons-io-2.20.0...rel/commons-io-2.21.0) Updates `org.apache.commons:commons-lang3` from 3.19.0 to 3.20.0 Updates `commons-validator:commons-validator` from 1.10.0 to 1.10.1 - [Changelog](https://github.com/apache/commons-validator/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-validator/compare/rel/commons-validator-1.10.0...rel/commons-validator-1.10.1) --- updated-dependencies: - dependency-name: commons-cli:commons-cli dependency-version: 1.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-codec:commons-codec dependency-version: 1.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-configuration2 dependency-version: 2.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-io:commons-io dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: org.apache.commons:commons-lang3 dependency-version: 3.20.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: apache-commons - dependency-name: commons-validator:commons-validator dependency-version: 1.10.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: apache-commons ... Signed-off-by: dependabot[bot] --- pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 61b167bd6a8d..ccebc256c8ff 100644 --- a/pom.xml +++ b/pom.xml @@ -1471,12 +1471,12 @@ commons-cli commons-cli - 1.10.0 + 1.11.0 commons-codec commons-codec - 1.19.0 + 1.20.0 org.apache.commons @@ -1486,7 +1486,7 @@ org.apache.commons commons-configuration2 - 2.12.0 + 2.13.0 org.apache.commons @@ -1503,12 +1503,12 @@ commons-io commons-io - 2.20.0 + 2.21.0 org.apache.commons commons-lang3 - 3.19.0 + 3.20.0 @@ -1540,7 +1540,7 @@ commons-validator commons-validator - 1.10.0 + 1.10.1 jakarta.activation From 6d59f247847b186d99e3d688579e550a16984732 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:52:37 +0000 Subject: [PATCH 454/701] build(deps): bump the build-tools group across 1 directory with 10 updates Bumps the build-tools group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [com.github.spotbugs:spotbugs](https://github.com/spotbugs/spotbugs) | `4.9.6` | `4.9.8` | | [org.apache.maven.plugins:maven-jar-plugin](https://github.com/apache/maven-jar-plugin) | `3.4.2` | `3.5.0` | | [org.apache.maven.plugins:maven-war-plugin](https://github.com/apache/maven-war-plugin) | `3.4.0` | `3.5.0` | | [com.github.spotbugs:spotbugs-maven-plugin](https://github.com/spotbugs/spotbugs-maven-plugin) | `4.9.6.0` | `4.9.8.2` | | [org.apache.maven.plugins:maven-assembly-plugin](https://github.com/apache/maven-assembly-plugin) | `3.7.1` | `3.8.0` | | [org.apache.maven.plugins:maven-resources-plugin](https://github.com/apache/maven-resources-plugin) | `3.3.1` | `3.4.0` | | [org.apache.maven.plugins:maven-source-plugin](https://github.com/apache/maven-source-plugin) | `3.3.1` | `3.4.0` | | [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) | `0.8.13` | `0.8.14` | | [org.apache.maven.plugins:maven-release-plugin](https://github.com/apache/maven-release) | `3.1.1` | `3.2.0` | | [org.codehaus.mojo:xml-maven-plugin](https://github.com/mojohaus/xml-maven-plugin) | `1.1.0` | `1.2.0` | Updates `com.github.spotbugs:spotbugs` from 4.9.6 to 4.9.8 - [Release notes](https://github.com/spotbugs/spotbugs/releases) - [Changelog](https://github.com/spotbugs/spotbugs/blob/master/CHANGELOG.md) - [Commits](https://github.com/spotbugs/spotbugs/compare/4.9.6...4.9.8) Updates `org.apache.maven.plugins:maven-jar-plugin` from 3.4.2 to 3.5.0 - [Release notes](https://github.com/apache/maven-jar-plugin/releases) - [Commits](https://github.com/apache/maven-jar-plugin/compare/maven-jar-plugin-3.4.2...maven-jar-plugin-3.5.0) Updates `org.apache.maven.plugins:maven-war-plugin` from 3.4.0 to 3.5.0 - [Release notes](https://github.com/apache/maven-war-plugin/releases) - [Commits](https://github.com/apache/maven-war-plugin/compare/maven-war-plugin-3.4.0...maven-war-plugin-3.5.0) Updates `com.github.spotbugs:spotbugs-maven-plugin` from 4.9.6.0 to 4.9.8.2 - [Release notes](https://github.com/spotbugs/spotbugs-maven-plugin/releases) - [Commits](https://github.com/spotbugs/spotbugs-maven-plugin/compare/spotbugs-maven-plugin-4.9.6.0...spotbugs-maven-plugin-4.9.8.2) Updates `org.apache.maven.plugins:maven-assembly-plugin` from 3.7.1 to 3.8.0 - [Release notes](https://github.com/apache/maven-assembly-plugin/releases) - [Commits](https://github.com/apache/maven-assembly-plugin/compare/maven-assembly-plugin-3.7.1...v3.8.0) Updates `org.apache.maven.plugins:maven-resources-plugin` from 3.3.1 to 3.4.0 - [Release notes](https://github.com/apache/maven-resources-plugin/releases) - [Commits](https://github.com/apache/maven-resources-plugin/compare/maven-resources-plugin-3.3.1...v3.4.0) Updates `org.apache.maven.plugins:maven-source-plugin` from 3.3.1 to 3.4.0 - [Release notes](https://github.com/apache/maven-source-plugin/releases) - [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-3.3.1...maven-source-plugin-3.4.0) Updates `org.jacoco:jacoco-maven-plugin` from 0.8.13 to 0.8.14 - [Release notes](https://github.com/jacoco/jacoco/releases) - [Commits](https://github.com/jacoco/jacoco/compare/v0.8.13...v0.8.14) Updates `org.apache.maven.plugins:maven-release-plugin` from 3.1.1 to 3.2.0 - [Release notes](https://github.com/apache/maven-release/releases) - [Commits](https://github.com/apache/maven-release/compare/maven-release-3.1.1...maven-release-3.2.0) Updates `org.codehaus.mojo:xml-maven-plugin` from 1.1.0 to 1.2.0 - [Release notes](https://github.com/mojohaus/xml-maven-plugin/releases) - [Commits](https://github.com/mojohaus/xml-maven-plugin/compare/1.1.0...1.2.0) --- updated-dependencies: - dependency-name: com.github.spotbugs:spotbugs dependency-version: 4.9.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-jar-plugin dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-war-plugin dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: com.github.spotbugs:spotbugs-maven-plugin dependency-version: 4.9.8.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-assembly-plugin dependency-version: 3.8.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-resources-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-source-plugin dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.jacoco:jacoco-maven-plugin dependency-version: 0.8.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-release-plugin dependency-version: 3.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.codehaus.mojo:xml-maven-plugin dependency-version: 1.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 61b167bd6a8d..65c829051034 100644 --- a/pom.xml +++ b/pom.xml @@ -177,7 +177,7 @@ org.apache.maven.plugins maven-jar-plugin - 3.4.2 + 3.5.0 @@ -191,7 +191,7 @@ org.apache.maven.plugins maven-war-plugin - 3.4.0 + 3.5.1 false @@ -303,7 +303,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.9.6.0 + 4.9.8.2 Max Low @@ -313,7 +313,7 @@ com.github.spotbugs spotbugs - 4.9.6 + 4.9.8 @@ -343,7 +343,7 @@ maven-assembly-plugin - 3.7.1 + 3.8.0 org.apache.maven.plugins @@ -353,7 +353,7 @@ org.apache.maven.plugins maven-resources-plugin - 3.3.1 + 3.4.0 @@ -386,7 +386,7 @@ org.apache.maven.plugins maven-source-plugin - 3.3.1 + 3.4.0 @@ -405,7 +405,7 @@ org.jacoco jacoco-maven-plugin - 0.8.13 + 0.8.14 @@ -417,7 +417,7 @@ org.apache.maven.plugins maven-release-plugin - 3.1.1 + 3.2.0 @@ -485,7 +485,7 @@ org.codehaus.mojo xml-maven-plugin - 1.1.0 + 1.2.0 validate-ALL-xml-and-xsl From 8d848a1555c003d2ed55ad7be525954a10698249 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Thu, 7 Aug 2025 11:20:18 +0300 Subject: [PATCH 455/701] pom.xml: upgrade pdfbox and tika Use latest pdfbox 3.0.5 and tika 3.2.2. See: https://pdfbox.apache.org/3.0/migration.html See: https://dist.apache.org/repos/dist/release/tika/3.2.2/CHANGES-3.2.2.txt --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 512254a9a323..6ee3ac3d46dc 100644 --- a/pom.xml +++ b/pom.xml @@ -39,10 +39,10 @@ 9.4.58.v20250814 2.25.2 - 2.0.35 + 3.0.5 1.19.0 1.7.36 - 2.9.4 + 3.2.2 1.82 From a44c9bbcca6ce8b76be7af3815f9986451b1e5e2 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Thu, 7 Aug 2025 11:39:09 +0300 Subject: [PATCH 456/701] dspace-api: Update syntax for pdfbox 3.0.x Conflicts resolved with CitationDocumentServiceImpl to satisfy the older implementation. --- .../ImageMagickThumbnailFilter.java | 6 ++--- .../app/mediafilter/PDFBoxThumbnail.java | 4 +++- .../dspace/content/packager/PDFPackager.java | 23 +++++++++++-------- .../EpoImportMetadataSourceServiceImpl.java | 2 +- .../app/rest/BitstreamRestControllerIT.java | 6 +++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java index 408982d157e5..7543410a7968 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java @@ -14,7 +14,7 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.dspace.content.Bitstream; @@ -153,8 +153,8 @@ public File getImageFile(File f, boolean verbose) // the CropBox is missing or empty because pdfbox will set it to the // same size as the MediaBox if it doesn't exist. Also note that we // only need to check the first page, since that's what we use for - // generating the thumbnail (PDDocument uses a zero-based index). - PDPage pdfPage = PDDocument.load(f).getPage(0); + // generating the thumbnail (PDPage uses a zero-based index). + PDPage pdfPage = Loader.loadPDF(f).getPage(0); PDRectangle pdfPageMediaBox = pdfPage.getMediaBox(); PDRectangle pdfPageCropBox = pdfPage.getCropBox(); diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java index 577f1dec4a18..eb23e9daa085 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java @@ -11,6 +11,8 @@ import java.io.InputStream; import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException; import org.apache.pdfbox.rendering.PDFRenderer; @@ -71,7 +73,7 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo BufferedImage buf; // Render the page image. - try ( PDDocument doc = PDDocument.load(source); ) { + try ( PDDocument doc = Loader.loadPDF(new RandomAccessReadBuffer(source)); ) { PDFRenderer renderer = new PDFRenderer(doc); buf = renderer.renderImage(0); } catch (InvalidPasswordException ex) { diff --git a/dspace-api/src/main/java/org/dspace/content/packager/PDFPackager.java b/dspace-api/src/main/java/org/dspace/content/packager/PDFPackager.java index 6c7baad45497..f63585f3c498 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/PDFPackager.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/PDFPackager.java @@ -18,11 +18,11 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.io.MemoryUsageSetting; -import org.apache.pdfbox.io.RandomAccessBufferedFileInputStream; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.io.ScratchFile; -import org.apache.pdfbox.pdfparser.PDFParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.dspace.authorize.AuthorizeException; @@ -330,19 +330,24 @@ private void crosswalkPDF(Context context, Item item, InputStream metadata) COSDocument cos = null; try { - ScratchFile scratchFile = null; + PDDocument document = null; + try { - long useRAM = Runtime.getRuntime().freeMemory() * 80 / 100; // use up to 80% of JVM free memory - scratchFile = new ScratchFile( - MemoryUsageSetting.setupMixed(useRAM)); // then fallback to temp file (unlimited size) + // Use up to 80% of JVM free memory and fall back to a temp file (unlimited size) + long useRAM = Runtime.getRuntime().freeMemory() * 80 / 100; + document = Loader.loadPDF( + new RandomAccessReadBuffer(metadata), + () -> new ScratchFile(MemoryUsageSetting.setupMixed(useRAM))); } catch (IOException ioe) { log.warn("Error initializing scratch file: " + ioe.getMessage()); } - PDFParser parser = new PDFParser(new RandomAccessBufferedFileInputStream(metadata), scratchFile); - parser.parse(); - cos = parser.getDocument(); + // sanity check: loaded PDF document must not be null. + if (document == null) { + throw new MetadataValidationException("The provided stream could not be parsed into a PDF document."); + } + cos = document.getDocument(); // sanity check: PDFBox breaks on encrypted documents, so give up. if (cos.getEncryptionDictionary() != null) { throw new MetadataValidationException("This packager cannot accept an encrypted PDF document."); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index 552f607827a8..4ec1f4db39e7 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -29,9 +29,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpException; import org.apache.http.client.utils.URIBuilder; +import org.apache.jena.ext.xerces.impl.dv.util.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.xerces.impl.dv.util.Base64; import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 095d4d89fb1f..b4649ce4c5b6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -56,6 +56,8 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.solr.client.solrj.SolrServerException; @@ -994,7 +996,7 @@ private String extractPDFText(byte[] content) throws IOException { try (ByteArrayInputStream source = new ByteArrayInputStream(content); Writer writer = new StringWriter(); - PDDocument pdfDoc = PDDocument.load(source)) { + PDDocument pdfDoc = Loader.loadPDF(new RandomAccessReadBuffer(source))) { pts.writeText(pdfDoc, writer); return writer.toString(); @@ -1003,7 +1005,7 @@ private String extractPDFText(byte[] content) throws IOException { private int getNumberOfPdfPages(byte[] content) throws IOException { try (ByteArrayInputStream source = new ByteArrayInputStream(content); - PDDocument pdfDoc = PDDocument.load(source)) { + PDDocument pdfDoc = Loader.loadPDF(new RandomAccessReadBuffer(source))) { return pdfDoc.getNumberOfPages(); } } From 461cd778af3fb99132476e91fc1edaaa0da6154c Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 10 Dec 2025 17:56:36 +0100 Subject: [PATCH 457/701] Apply PDFBox 3.x changes to CitationDocumentServiceImpl --- .../disseminate/CitationDocumentServiceImpl.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java index b1441867772f..bf9f7b9d6a01 100644 --- a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java @@ -23,6 +23,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; @@ -30,6 +32,7 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bitstream; @@ -304,7 +307,7 @@ public Pair makeCitedDocument(Context context, Bitstream bitstream Item item = (Item) bitstreamService.getParentObject(context, bitstream); final InputStream inputStream = bitstreamService.retrieve(context, bitstream); try { - sourceDocument = sourceDocument.load(inputStream); + sourceDocument = Loader.loadPDF(new RandomAccessReadBuffer(inputStream)); } finally { inputStream.close(); } @@ -335,9 +338,10 @@ protected void generateCoverPage(Context context, PDDocument document, PDPage co int xwidth = 550; int ygap = 20; - PDFont fontHelvetica = PDType1Font.HELVETICA; - PDFont fontHelveticaBold = PDType1Font.HELVETICA_BOLD; - PDFont fontHelveticaOblique = PDType1Font.HELVETICA_OBLIQUE; + PDFont fontHelvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + PDFont fontHelveticaBold = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); + PDFont fontHelveticaOblique = new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE); + contentStream.setNonStrokingColor(Color.BLACK); String[][] content = {header1}; From ba927cec47174b8bfc35ced93cafc5adfe177023 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:56:59 +0000 Subject: [PATCH 458/701] Bump tika.version from 3.2.2 to 3.2.3 Bumps `tika.version` from 3.2.2 to 3.2.3. Updates `org.apache.tika:tika-core` from 3.2.2 to 3.2.3 - [Changelog](https://github.com/apache/tika/blob/main/CHANGES.txt) - [Commits](https://github.com/apache/tika/compare/3.2.2...3.2.3) Updates `org.apache.tika:tika-parsers-standard-package` from 3.2.2 to 3.2.3 --- updated-dependencies: - dependency-name: org.apache.tika:tika-core dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.tika:tika-parsers-standard-package dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6ee3ac3d46dc..eaaf4d9d017d 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ 3.0.5 1.19.0 1.7.36 - 3.2.2 + 3.2.3 1.82 From 903e8d43c641b770e15d82b717794d8eef90581c Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 10 Dec 2025 18:08:30 +0100 Subject: [PATCH 459/701] Revert org.apache.xerces import change --- .../epo/service/EpoImportMetadataSourceServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index 4ec1f4db39e7..552f607827a8 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -29,9 +29,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpException; import org.apache.http.client.utils.URIBuilder; -import org.apache.jena.ext.xerces.impl.dv.util.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.xerces.impl.dv.util.Base64; import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; From 97ddacccfcb65bd86f7f14ab9f1cf2fe5f279810 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Thu, 7 Aug 2025 11:20:18 +0300 Subject: [PATCH 460/701] pom.xml: upgrade pdfbox and tika Use latest pdfbox 3.0.5 and tika 3.2.2. See: https://pdfbox.apache.org/3.0/migration.html See: https://dist.apache.org/repos/dist/release/tika/3.2.2/CHANGES-3.2.2.txt --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 966af2edb2dd..401a83b744d6 100644 --- a/pom.xml +++ b/pom.xml @@ -40,10 +40,10 @@ 9.4.58.v20250814 2.25.2 - 2.0.35 + 3.0.5 1.19.0 2.0.17 - 2.9.4 + 3.2.2 1.82 8.0.1 From ab7288e75b18520de6a85955dc5369b93c96be85 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Thu, 7 Aug 2025 11:39:09 +0300 Subject: [PATCH 461/701] dspace-api: Update syntax for pdfbox 3.0.x Conflicts resolved with CitationDocumentServiceImpl to satisfy the older implementation. --- .../ImageMagickThumbnailFilter.java | 6 ++--- .../app/mediafilter/PDFBoxThumbnail.java | 4 +++- .../dspace/content/packager/PDFPackager.java | 23 +++++++++++-------- .../EpoImportMetadataSourceServiceImpl.java | 2 +- .../app/rest/BitstreamRestControllerIT.java | 6 +++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java index 408982d157e5..7543410a7968 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java @@ -14,7 +14,7 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.dspace.content.Bitstream; @@ -153,8 +153,8 @@ public File getImageFile(File f, boolean verbose) // the CropBox is missing or empty because pdfbox will set it to the // same size as the MediaBox if it doesn't exist. Also note that we // only need to check the first page, since that's what we use for - // generating the thumbnail (PDDocument uses a zero-based index). - PDPage pdfPage = PDDocument.load(f).getPage(0); + // generating the thumbnail (PDPage uses a zero-based index). + PDPage pdfPage = Loader.loadPDF(f).getPage(0); PDRectangle pdfPageMediaBox = pdfPage.getMediaBox(); PDRectangle pdfPageCropBox = pdfPage.getCropBox(); diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java index 577f1dec4a18..eb23e9daa085 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/PDFBoxThumbnail.java @@ -11,6 +11,8 @@ import java.io.InputStream; import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException; import org.apache.pdfbox.rendering.PDFRenderer; @@ -71,7 +73,7 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo BufferedImage buf; // Render the page image. - try ( PDDocument doc = PDDocument.load(source); ) { + try ( PDDocument doc = Loader.loadPDF(new RandomAccessReadBuffer(source)); ) { PDFRenderer renderer = new PDFRenderer(doc); buf = renderer.renderImage(0); } catch (InvalidPasswordException ex) { diff --git a/dspace-api/src/main/java/org/dspace/content/packager/PDFPackager.java b/dspace-api/src/main/java/org/dspace/content/packager/PDFPackager.java index 6c7baad45497..f63585f3c498 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/PDFPackager.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/PDFPackager.java @@ -18,11 +18,11 @@ import org.apache.commons.lang3.ArrayUtils; import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.Loader; import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.io.MemoryUsageSetting; -import org.apache.pdfbox.io.RandomAccessBufferedFileInputStream; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.io.ScratchFile; -import org.apache.pdfbox.pdfparser.PDFParser; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.dspace.authorize.AuthorizeException; @@ -330,19 +330,24 @@ private void crosswalkPDF(Context context, Item item, InputStream metadata) COSDocument cos = null; try { - ScratchFile scratchFile = null; + PDDocument document = null; + try { - long useRAM = Runtime.getRuntime().freeMemory() * 80 / 100; // use up to 80% of JVM free memory - scratchFile = new ScratchFile( - MemoryUsageSetting.setupMixed(useRAM)); // then fallback to temp file (unlimited size) + // Use up to 80% of JVM free memory and fall back to a temp file (unlimited size) + long useRAM = Runtime.getRuntime().freeMemory() * 80 / 100; + document = Loader.loadPDF( + new RandomAccessReadBuffer(metadata), + () -> new ScratchFile(MemoryUsageSetting.setupMixed(useRAM))); } catch (IOException ioe) { log.warn("Error initializing scratch file: " + ioe.getMessage()); } - PDFParser parser = new PDFParser(new RandomAccessBufferedFileInputStream(metadata), scratchFile); - parser.parse(); - cos = parser.getDocument(); + // sanity check: loaded PDF document must not be null. + if (document == null) { + throw new MetadataValidationException("The provided stream could not be parsed into a PDF document."); + } + cos = document.getDocument(); // sanity check: PDFBox breaks on encrypted documents, so give up. if (cos.getEncryptionDictionary() != null) { throw new MetadataValidationException("This packager cannot accept an encrypted PDF document."); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index 552f607827a8..4ec1f4db39e7 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -29,9 +29,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpException; import org.apache.http.client.utils.URIBuilder; +import org.apache.jena.ext.xerces.impl.dv.util.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.xerces.impl.dv.util.Base64; import org.dspace.app.util.XMLUtils; import org.dspace.content.Item; import org.dspace.importer.external.datamodel.ImportRecord; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 596e4e0c6b70..fe75c9fc518c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -57,6 +57,8 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.solr.client.solrj.SolrServerException; @@ -994,7 +996,7 @@ private String extractPDFText(byte[] content) throws IOException { try (ByteArrayInputStream source = new ByteArrayInputStream(content); Writer writer = new StringWriter(); - PDDocument pdfDoc = PDDocument.load(source)) { + PDDocument pdfDoc = Loader.loadPDF(new RandomAccessReadBuffer(source))) { pts.writeText(pdfDoc, writer); return writer.toString(); @@ -1003,7 +1005,7 @@ private String extractPDFText(byte[] content) throws IOException { private int getNumberOfPdfPages(byte[] content) throws IOException { try (ByteArrayInputStream source = new ByteArrayInputStream(content); - PDDocument pdfDoc = PDDocument.load(source)) { + PDDocument pdfDoc = Loader.loadPDF(new RandomAccessReadBuffer(source))) { return pdfDoc.getNumberOfPages(); } } From ea21c0f558e342ed01c92e17dfbb5378f67babd0 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 10 Dec 2025 17:56:36 +0100 Subject: [PATCH 462/701] Apply PDFBox 3.x changes to CitationDocumentServiceImpl --- .../disseminate/CitationDocumentServiceImpl.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java index b1441867772f..bf9f7b9d6a01 100644 --- a/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/disseminate/CitationDocumentServiceImpl.java @@ -23,6 +23,8 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.io.RandomAccessReadBuffer; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; @@ -30,6 +32,7 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bitstream; @@ -304,7 +307,7 @@ public Pair makeCitedDocument(Context context, Bitstream bitstream Item item = (Item) bitstreamService.getParentObject(context, bitstream); final InputStream inputStream = bitstreamService.retrieve(context, bitstream); try { - sourceDocument = sourceDocument.load(inputStream); + sourceDocument = Loader.loadPDF(new RandomAccessReadBuffer(inputStream)); } finally { inputStream.close(); } @@ -335,9 +338,10 @@ protected void generateCoverPage(Context context, PDDocument document, PDPage co int xwidth = 550; int ygap = 20; - PDFont fontHelvetica = PDType1Font.HELVETICA; - PDFont fontHelveticaBold = PDType1Font.HELVETICA_BOLD; - PDFont fontHelveticaOblique = PDType1Font.HELVETICA_OBLIQUE; + PDFont fontHelvetica = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + PDFont fontHelveticaBold = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); + PDFont fontHelveticaOblique = new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE); + contentStream.setNonStrokingColor(Color.BLACK); String[][] content = {header1}; From c3de82d3732c1ece3d61017310aee8dc8c5939f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:56:59 +0000 Subject: [PATCH 463/701] Bump tika.version from 3.2.2 to 3.2.3 Bumps `tika.version` from 3.2.2 to 3.2.3. Updates `org.apache.tika:tika-core` from 3.2.2 to 3.2.3 - [Changelog](https://github.com/apache/tika/blob/main/CHANGES.txt) - [Commits](https://github.com/apache/tika/compare/3.2.2...3.2.3) Updates `org.apache.tika:tika-parsers-standard-package` from 3.2.2 to 3.2.3 --- updated-dependencies: - dependency-name: org.apache.tika:tika-core dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.tika:tika-parsers-standard-package dependency-version: 3.2.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 401a83b744d6..d4e45b05cde5 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 3.0.5 1.19.0 2.0.17 - 3.2.2 + 3.2.3 1.82 8.0.1 From 686b06f5abeee96a8e32873d4f4ef35e358a394d Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Wed, 10 Dec 2025 15:04:16 +0100 Subject: [PATCH 464/701] [DURACOM-428] fix closed http client before reading entity (cherry picked from commit 5ac1c4a863b02bcdf7cf034d73c7aaa56952de52) --- .../impl/OrcidV3AuthorDataProvider.java | 3 + .../model/factory/OrcidFactoryUtils.java | 106 ++++++++++++------ 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index a9e10f92948d..fad0e75b4fb7 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -89,6 +89,9 @@ public void init() throws IOException { public void initializeAccessToken() { // If we have reaches max retries or the access token is already set, return immediately if (maxClientRetries <= 0 || StringUtils.isNotBlank(accessToken)) { + if (maxClientRetries <= 0) { + log.warn("Maximum retry attempts reached for ORCID token retrieval"); + } return; } try { diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java index ce68ab47c26e..f08aff740580 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java @@ -7,21 +7,29 @@ */ package org.dspace.orcid.model.factory; -import java.io.BufferedReader; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; import org.json.JSONObject; +import org.json.JSONTokener; /** * Utility class for Orcid factory classes. This is used to parse the @@ -29,13 +37,12 @@ * contributors and external ids configuration). * * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * */ public final class OrcidFactoryUtils { - private OrcidFactoryUtils() { + private static final Logger log = LogManager.getLogger(OrcidFactoryUtils.class); - } + private OrcidFactoryUtils() { } /** * Parse the given configurations value and returns a map with metadata fields @@ -46,7 +53,7 @@ private OrcidFactoryUtils() { * @return the configurations parsing result as map */ public static Map parseConfigurations(String configurations) { - Map configurationMap = new HashMap(); + Map configurationMap = new HashMap<>(); if (StringUtils.isBlank(configurations)) { return configurationMap; } @@ -55,7 +62,6 @@ public static Map parseConfigurations(String configurations) { String[] configurationSections = parseConfiguration(configuration); configurationMap.put(configurationSections[0], configurationSections[1]); } - return configurationMap; } @@ -87,37 +93,65 @@ private static String[] parseConfiguration(String configuration) { */ public static Optional retrieveAccessToken(String clientId, String clientSecret, String oauthUrl) throws IOException { - if (StringUtils.isNotBlank(clientSecret) && StringUtils.isNotBlank(clientId) - && StringUtils.isNotBlank(oauthUrl)) { - String authenticationParameters = "?client_id=" + clientId + - "&client_secret=" + clientSecret + - "&scope=/read-public&grant_type=client_credentials"; - HttpPost httpPost = new HttpPost(oauthUrl + authenticationParameters); - httpPost.addHeader("Accept", "application/json"); - httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); - - HttpResponse response; - try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { - response = httpClient.execute(httpPost); + if (StringUtils.isBlank(clientSecret) || StringUtils.isBlank(clientId) || StringUtils.isBlank(oauthUrl)) { + String missingParams = (StringUtils.isBlank(clientId) ? "clientId " : "") + + (StringUtils.isBlank(clientSecret) ? "clientSecret " : "") + + (StringUtils.isBlank(oauthUrl) ? "oauthUrl" : ""); + log.error("Cannot retrieve ORCID access token: missing required parameters:{} ", missingParams.trim()); + return Optional.empty(); + } + + HttpPost httpPost = new HttpPost(oauthUrl); + + String auth = clientId + ":" + clientSecret; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(UTF_8)); + addHeaders(httpPost, encodedAuth); + + List params = new ArrayList<>(); + params.add(new BasicNameValuePair("grant_type", "client_credentials")); + params.add(new BasicNameValuePair("scope", "/read-public")); + httpPost.setEntity(new UrlEncodedFormEntity(params, UTF_8)); + + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + log.debug("Sending ORCID token request to {}", oauthUrl); + HttpResponse response = httpClient.execute(httpPost); + if (!isSuccessful(response)) { + log.error("Failed to retrieve ORCID access token"); + return Optional.empty(); } - JSONObject responseObject = null; - if (response != null && response.getStatusLine().getStatusCode() == 200) { - try (InputStream is = response.getEntity().getContent(); - BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, - StandardCharsets.UTF_8))) { - String inputStr; - while ((inputStr = streamReader.readLine()) != null && responseObject == null) { - if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) { - responseObject = new JSONObject(inputStr); - } - } + // Parsing JSON response + try (InputStream is = response.getEntity().getContent()) { + JSONObject responseObject = new JSONObject(new JSONTokener(is)); + if (responseObject.has("access_token")) { + String token = responseObject.getString("access_token"); + log.debug("Successfully retrieved ORCID access token"); + return Optional.of(token); + } else { + log.error("ORCID response missing access_token field:{} ", responseObject); + return Optional.empty(); } } - if (responseObject != null && responseObject.has("access_token")) { - return Optional.of((String) responseObject.get("access_token")); - } } - // Return empty by default - return Optional.empty(); } + + private static void addHeaders(HttpPost httpPost, String encodedAuth) { + httpPost.addHeader("Authorization", "Basic " + encodedAuth); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); + } + + private static boolean isSuccessful(HttpResponse response) { + if (response == null) { + log.error("ORCID API request failed: null response received"); + return false; + } + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + var errorMsg = "ORCID API request failed with status code {}: {}"; + log.error(errorMsg, statusCode, response.getStatusLine().getReasonPhrase()); + return false; + } + return true; + } + } From d0e14fde74c0a039a07911b7d6dca72f104c7768 Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Wed, 10 Dec 2025 15:05:00 +0100 Subject: [PATCH 465/701] [DURACOM-428] improve ORCID connector (cherry picked from commit 768c669985ed106d819074c8f336f4c69cd7e568) --- .../dspace/external/OrcidRestConnector.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index aa16af7a524d..1e9688aa4ce6 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -9,13 +9,13 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Scanner; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -28,9 +28,6 @@ */ public class OrcidRestConnector { - /** - * log4j logger - */ private static final Logger log = LogManager.getLogger(OrcidRestConnector.class); private final String url; @@ -40,32 +37,31 @@ public OrcidRestConnector(String url) { } public InputStream get(String path, String accessToken) { - CloseableHttpResponse getResponse = null; - InputStream result = null; - path = trimSlashes(path); - - String fullPath = url + '/' + path; + String fullPath = url + '/' + trimSlashes(path); HttpGet httpGet = new HttpGet(fullPath); if (StringUtils.isNotBlank(accessToken)) { httpGet.addHeader("Content-Type", "application/vnd.orcid+xml"); httpGet.addHeader("Authorization","Bearer " + accessToken); } try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { - getResponse = httpClient.execute(httpGet); - try (InputStream responseStream = getResponse.getEntity().getContent()) { + CloseableHttpResponse httpResponse = httpClient.execute(httpGet); + if (!isSuccessful(httpResponse)) { + // Consume entity to avoid memory leak + EntityUtils.consumeQuietly(httpResponse.getEntity()); + var statusCode = getStatusCode(httpResponse); + var reason = httpResponse.getStatusLine().getReasonPhrase(); + var error = String.format("The request failed with:%d code, reason:%s ", statusCode, reason); + throw new RuntimeException(error); + } + try (InputStream responseStream = httpResponse.getEntity().getContent()) { // Read all the content of the response stream into a byte array to prevent TruncatedChunkException byte[] content = responseStream.readAllBytes(); - result = new ByteArrayInputStream(content); + return new ByteArrayInputStream(content); } } catch (Exception e) { - getGotError(e, fullPath); + log.error("Error in rest connector for path: " + fullPath, e); + throw new RuntimeException("Failed to execute ORCID request: " + fullPath, e); } - - return result; - } - - protected void getGotError(Exception e, String fullPath) { - log.error("Error in rest connector for path: " + fullPath, e); } public static String trimSlashes(String path) { @@ -78,8 +74,13 @@ public static String trimSlashes(String path) { return path; } - public static String convertStreamToString(InputStream is) { - Scanner s = new Scanner(is, StandardCharsets.UTF_8).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; + private boolean isSuccessful(HttpResponse response) { + int statusCode = getStatusCode(response); + return statusCode >= 200 || statusCode <= 299; + } + + private int getStatusCode(HttpResponse response) { + return response.getStatusLine().getStatusCode(); } + } From a1e107db7ef4b0c135e0022f244ea9bca71af4be Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Wed, 10 Dec 2025 17:30:23 +0100 Subject: [PATCH 466/701] [DURACOM-428] implemented custom exception for orcid connector (cherry picked from commit e3187bf201b82bec7ab1506ea10f6444c75e9c83) --- .../orcid/Orcidv3SolrAuthorityImpl.java | 58 ++++++++++++------- .../external/OrcidConnectionException.java | 33 +++++++++++ .../dspace/external/OrcidRestConnector.java | 34 +++++------ .../impl/OrcidV3AuthorDataProvider.java | 51 ++++++++++------ .../org/dspace/authority/orcid/MockOrcid.java | 7 ++- 5 files changed, 124 insertions(+), 59 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java diff --git a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java index 494daa97734a..312a00c146af 100644 --- a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java +++ b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -20,6 +21,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.authority.AuthorityValue; import org.dspace.authority.SolrAuthorityInterface; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.dspace.external.provider.orcid.xml.XMLtoBio; import org.dspace.orcid.model.factory.OrcidFactoryUtils; @@ -142,9 +144,15 @@ public Person getBio(String id) { return null; } initializeAccessToken(); - InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - XMLtoBio converter = new XMLtoBio(); - return converter.convertSinglePerson(bioDocument); + try { + InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), + accessToken); + XMLtoBio converter = new XMLtoBio(); + return converter.convertSinglePerson(bioDocument); + } catch (OrcidConnectionException e) { + log.error("Error retrieving ORCID bio for ID=" + id, e); + return null; + } } @@ -167,29 +175,35 @@ public List queryBio(String text, int start, int rows) { // Check / init access token initializeAccessToken(); - String searchPath = "search?q=" + URLEncoder.encode(text) + "&start=" + start + "&rows=" + rows; + String searchPath = "search?q=" + URLEncoder.encode(text, StandardCharsets.UTF_8) + "&start=" + start + + "&rows=" + rows; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - XMLtoBio converter = new XMLtoBio(); - List results = converter.convert(bioDocument); - List bios = new LinkedList<>(); - for (Result result : results) { - OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); - if (orcidIdentifier != null) { - log.debug("Found OrcidId=" + orcidIdentifier.toString()); - String orcid = orcidIdentifier.getPath(); - Person bio = getBio(orcid); - if (bio != null) { - bios.add(bio); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + XMLtoBio converter = new XMLtoBio(); + List results = converter.convert(bioDocument); + List bios = new LinkedList<>(); + for (Result result : results) { + OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); + if (orcidIdentifier != null) { + log.debug("Found OrcidId=" + orcidIdentifier); + String orcid = orcidIdentifier.getPath(); + Person bio = getBio(orcid); + if (bio != null) { + bios.add(bio); + } } } + try { + bioDocument.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return bios; + } catch (OrcidConnectionException e) { + log.error("Error searching ORCID for query=" + text, e); + return Collections.emptyList(); } - try { - bioDocument.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - return bios; } /** diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java b/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java new file mode 100644 index 000000000000..3574045aab2b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java @@ -0,0 +1,33 @@ +/** + * 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.external; + +/** + * Exception thrown when there are issues with ORCID service connections. + * + * @author Boychuk Mykhaylo (mykhaylo.boychuk@4science.com) + */ +public class OrcidConnectionException extends Exception { + + private final int statusCode; + + public OrcidConnectionException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + public OrcidConnectionException(String message, int statusCode, Throwable cause) { + super(message, cause); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index 1e9688aa4ce6..3d61462cc354 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -15,7 +15,6 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -36,7 +35,7 @@ public OrcidRestConnector(String url) { this.url = url; } - public InputStream get(String path, String accessToken) { + public InputStream get(String path, String accessToken) throws OrcidConnectionException { String fullPath = url + '/' + trimSlashes(path); HttpGet httpGet = new HttpGet(fullPath); if (StringUtils.isNotBlank(accessToken)) { @@ -44,23 +43,24 @@ public InputStream get(String path, String accessToken) { httpGet.addHeader("Authorization","Bearer " + accessToken); } try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { - CloseableHttpResponse httpResponse = httpClient.execute(httpGet); - if (!isSuccessful(httpResponse)) { - // Consume entity to avoid memory leak - EntityUtils.consumeQuietly(httpResponse.getEntity()); - var statusCode = getStatusCode(httpResponse); - var reason = httpResponse.getStatusLine().getReasonPhrase(); - var error = String.format("The request failed with:%d code, reason:%s ", statusCode, reason); - throw new RuntimeException(error); - } - try (InputStream responseStream = httpResponse.getEntity().getContent()) { - // Read all the content of the response stream into a byte array to prevent TruncatedChunkException - byte[] content = responseStream.readAllBytes(); - return new ByteArrayInputStream(content); + try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) { + if (!isSuccessful(httpResponse)) { + var statusCode = getStatusCode(httpResponse); + var reason = httpResponse.getStatusLine().getReasonPhrase(); + var error = String.format("The request failed with:%d code, reason:%s ", statusCode, reason); + throw new OrcidConnectionException(error, statusCode); + } + try (InputStream responseStream = httpResponse.getEntity().getContent()) { + // Read all the content of the response stream into a byte array to prevent TruncatedChunkException + byte[] content = responseStream.readAllBytes(); + return new ByteArrayInputStream(content); + } } + } catch (OrcidConnectionException e) { + throw e; } catch (Exception e) { log.error("Error in rest connector for path: " + fullPath, e); - throw new RuntimeException("Failed to execute ORCID request: " + fullPath, e); + throw new OrcidConnectionException("Failed to execute ORCID request: " + fullPath, 0, e); } } @@ -83,4 +83,4 @@ private int getStatusCode(HttpResponse response) { return response.getStatusLine().getStatusCode(); } -} +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index fad0e75b4fb7..fe2fdfa95381 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -21,6 +21,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.AbstractExternalDataProvider; @@ -171,8 +172,14 @@ public Person getBio(String id) { return null; } initializeAccessToken(); - InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - return converter.convertSinglePerson(bioDocument); + try { + InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), + accessToken); + return converter.convertSinglePerson(bioDocument); + } catch (OrcidConnectionException e) { + log.error("Error retrieving ORCID bio for ID=" + id, e); + return null; + } } /** @@ -203,21 +210,26 @@ public List searchExternalDataObjects(String query, int star + "&start=" + start + "&rows=" + limit; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - List results = converter.convert(bioDocument); - List bios = new LinkedList<>(); - for (Result result : results) { - OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); - if (orcidIdentifier != null) { - log.debug("Found OrcidId=" + orcidIdentifier.getPath()); - String orcid = orcidIdentifier.getPath(); - Person bio = getBio(orcid); - if (bio != null) { - bios.add(bio); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + List results = converter.convert(bioDocument); + List bios = new LinkedList<>(); + for (Result result : results) { + OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); + if (orcidIdentifier != null) { + log.debug("Found OrcidId=" + orcidIdentifier.getPath()); + String orcid = orcidIdentifier.getPath(); + Person bio = getBio(orcid); + if (bio != null) { + bios.add(bio); + } } } + return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); + } catch (OrcidConnectionException e) { + log.error("Error searching ORCID for query=" + query, e); + return Collections.emptyList(); } - return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); } @Override @@ -236,8 +248,13 @@ public int getNumberOfResults(String query) { + "&start=" + 0 + "&rows=" + 0; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - return Math.min(converter.getNumberOfResultsFromXml(bioDocument), MAX_INDEX); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + return Math.min(converter.getNumberOfResultsFromXml(bioDocument), MAX_INDEX); + } catch (OrcidConnectionException e) { + log.error("Error getting number of results from ORCID for query=" + query, e); + return 0; + } } @@ -299,4 +316,4 @@ public void setOrcidRestConnector(OrcidRestConnector orcidRestConnector) { this.orcidRestConnector = orcidRestConnector; } -} +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java index 511df79f1e50..b6be6d1f3ac2 100644 --- a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java +++ b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java @@ -11,6 +11,7 @@ import java.io.InputStream; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -38,7 +39,7 @@ public void init() { * Call this to set up mocking for any test classes that need it. We don't set it in init() * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing */ - public void setupNoResultsSearch() { + public void setupNoResultsSearch() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override @@ -51,7 +52,7 @@ public InputStream answer(InvocationOnMock invocation) { * Call this to set up mocking for any test classes that need it. We don't set it in init() * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing */ - public void setupSingleSearch() { + public void setupSingleSearch() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?q=Bollini"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override @@ -64,7 +65,7 @@ public InputStream answer(InvocationOnMock invocation) { * Call this to set up mocking for any test classes that need it. We don't set it in init() * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing */ - public void setupSearchWithResults() { + public void setupSearchWithResults() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override From 69797d83d4b27ccfc66d8554fbe69a9572ca0ca1 Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Wed, 10 Dec 2025 15:04:16 +0100 Subject: [PATCH 467/701] [DURACOM-428] fix closed http client before reading entity (cherry picked from commit 5ac1c4a863b02bcdf7cf034d73c7aaa56952de52) --- .../impl/OrcidV3AuthorDataProvider.java | 3 + .../model/factory/OrcidFactoryUtils.java | 106 ++++++++++++------ 2 files changed, 73 insertions(+), 36 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index a9e10f92948d..fad0e75b4fb7 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -89,6 +89,9 @@ public void init() throws IOException { public void initializeAccessToken() { // If we have reaches max retries or the access token is already set, return immediately if (maxClientRetries <= 0 || StringUtils.isNotBlank(accessToken)) { + if (maxClientRetries <= 0) { + log.warn("Maximum retry attempts reached for ORCID token retrieval"); + } return; } try { diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java index ce68ab47c26e..f08aff740580 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/OrcidFactoryUtils.java @@ -7,21 +7,29 @@ */ package org.dspace.orcid.model.factory; -import java.io.BufferedReader; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; import org.json.JSONObject; +import org.json.JSONTokener; /** * Utility class for Orcid factory classes. This is used to parse the @@ -29,13 +37,12 @@ * contributors and external ids configuration). * * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * */ public final class OrcidFactoryUtils { - private OrcidFactoryUtils() { + private static final Logger log = LogManager.getLogger(OrcidFactoryUtils.class); - } + private OrcidFactoryUtils() { } /** * Parse the given configurations value and returns a map with metadata fields @@ -46,7 +53,7 @@ private OrcidFactoryUtils() { * @return the configurations parsing result as map */ public static Map parseConfigurations(String configurations) { - Map configurationMap = new HashMap(); + Map configurationMap = new HashMap<>(); if (StringUtils.isBlank(configurations)) { return configurationMap; } @@ -55,7 +62,6 @@ public static Map parseConfigurations(String configurations) { String[] configurationSections = parseConfiguration(configuration); configurationMap.put(configurationSections[0], configurationSections[1]); } - return configurationMap; } @@ -87,37 +93,65 @@ private static String[] parseConfiguration(String configuration) { */ public static Optional retrieveAccessToken(String clientId, String clientSecret, String oauthUrl) throws IOException { - if (StringUtils.isNotBlank(clientSecret) && StringUtils.isNotBlank(clientId) - && StringUtils.isNotBlank(oauthUrl)) { - String authenticationParameters = "?client_id=" + clientId + - "&client_secret=" + clientSecret + - "&scope=/read-public&grant_type=client_credentials"; - HttpPost httpPost = new HttpPost(oauthUrl + authenticationParameters); - httpPost.addHeader("Accept", "application/json"); - httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); - - HttpResponse response; - try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { - response = httpClient.execute(httpPost); + if (StringUtils.isBlank(clientSecret) || StringUtils.isBlank(clientId) || StringUtils.isBlank(oauthUrl)) { + String missingParams = (StringUtils.isBlank(clientId) ? "clientId " : "") + + (StringUtils.isBlank(clientSecret) ? "clientSecret " : "") + + (StringUtils.isBlank(oauthUrl) ? "oauthUrl" : ""); + log.error("Cannot retrieve ORCID access token: missing required parameters:{} ", missingParams.trim()); + return Optional.empty(); + } + + HttpPost httpPost = new HttpPost(oauthUrl); + + String auth = clientId + ":" + clientSecret; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(UTF_8)); + addHeaders(httpPost, encodedAuth); + + List params = new ArrayList<>(); + params.add(new BasicNameValuePair("grant_type", "client_credentials")); + params.add(new BasicNameValuePair("scope", "/read-public")); + httpPost.setEntity(new UrlEncodedFormEntity(params, UTF_8)); + + try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { + log.debug("Sending ORCID token request to {}", oauthUrl); + HttpResponse response = httpClient.execute(httpPost); + if (!isSuccessful(response)) { + log.error("Failed to retrieve ORCID access token"); + return Optional.empty(); } - JSONObject responseObject = null; - if (response != null && response.getStatusLine().getStatusCode() == 200) { - try (InputStream is = response.getEntity().getContent(); - BufferedReader streamReader = new BufferedReader(new InputStreamReader(is, - StandardCharsets.UTF_8))) { - String inputStr; - while ((inputStr = streamReader.readLine()) != null && responseObject == null) { - if (inputStr.startsWith("{") && inputStr.endsWith("}") && inputStr.contains("access_token")) { - responseObject = new JSONObject(inputStr); - } - } + // Parsing JSON response + try (InputStream is = response.getEntity().getContent()) { + JSONObject responseObject = new JSONObject(new JSONTokener(is)); + if (responseObject.has("access_token")) { + String token = responseObject.getString("access_token"); + log.debug("Successfully retrieved ORCID access token"); + return Optional.of(token); + } else { + log.error("ORCID response missing access_token field:{} ", responseObject); + return Optional.empty(); } } - if (responseObject != null && responseObject.has("access_token")) { - return Optional.of((String) responseObject.get("access_token")); - } } - // Return empty by default - return Optional.empty(); } + + private static void addHeaders(HttpPost httpPost, String encodedAuth) { + httpPost.addHeader("Authorization", "Basic " + encodedAuth); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded"); + } + + private static boolean isSuccessful(HttpResponse response) { + if (response == null) { + log.error("ORCID API request failed: null response received"); + return false; + } + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + var errorMsg = "ORCID API request failed with status code {}: {}"; + log.error(errorMsg, statusCode, response.getStatusLine().getReasonPhrase()); + return false; + } + return true; + } + } From 00e2d95f06d8eb7b21c7fd344d0bc1ddbd6e11ce Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Wed, 10 Dec 2025 15:05:00 +0100 Subject: [PATCH 468/701] [DURACOM-428] improve ORCID connector (cherry picked from commit 768c669985ed106d819074c8f336f4c69cd7e568) --- .../dspace/external/OrcidRestConnector.java | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index aa16af7a524d..1e9688aa4ce6 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -9,13 +9,13 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Scanner; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -28,9 +28,6 @@ */ public class OrcidRestConnector { - /** - * log4j logger - */ private static final Logger log = LogManager.getLogger(OrcidRestConnector.class); private final String url; @@ -40,32 +37,31 @@ public OrcidRestConnector(String url) { } public InputStream get(String path, String accessToken) { - CloseableHttpResponse getResponse = null; - InputStream result = null; - path = trimSlashes(path); - - String fullPath = url + '/' + path; + String fullPath = url + '/' + trimSlashes(path); HttpGet httpGet = new HttpGet(fullPath); if (StringUtils.isNotBlank(accessToken)) { httpGet.addHeader("Content-Type", "application/vnd.orcid+xml"); httpGet.addHeader("Authorization","Bearer " + accessToken); } try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { - getResponse = httpClient.execute(httpGet); - try (InputStream responseStream = getResponse.getEntity().getContent()) { + CloseableHttpResponse httpResponse = httpClient.execute(httpGet); + if (!isSuccessful(httpResponse)) { + // Consume entity to avoid memory leak + EntityUtils.consumeQuietly(httpResponse.getEntity()); + var statusCode = getStatusCode(httpResponse); + var reason = httpResponse.getStatusLine().getReasonPhrase(); + var error = String.format("The request failed with:%d code, reason:%s ", statusCode, reason); + throw new RuntimeException(error); + } + try (InputStream responseStream = httpResponse.getEntity().getContent()) { // Read all the content of the response stream into a byte array to prevent TruncatedChunkException byte[] content = responseStream.readAllBytes(); - result = new ByteArrayInputStream(content); + return new ByteArrayInputStream(content); } } catch (Exception e) { - getGotError(e, fullPath); + log.error("Error in rest connector for path: " + fullPath, e); + throw new RuntimeException("Failed to execute ORCID request: " + fullPath, e); } - - return result; - } - - protected void getGotError(Exception e, String fullPath) { - log.error("Error in rest connector for path: " + fullPath, e); } public static String trimSlashes(String path) { @@ -78,8 +74,13 @@ public static String trimSlashes(String path) { return path; } - public static String convertStreamToString(InputStream is) { - Scanner s = new Scanner(is, StandardCharsets.UTF_8).useDelimiter("\\A"); - return s.hasNext() ? s.next() : ""; + private boolean isSuccessful(HttpResponse response) { + int statusCode = getStatusCode(response); + return statusCode >= 200 || statusCode <= 299; + } + + private int getStatusCode(HttpResponse response) { + return response.getStatusLine().getStatusCode(); } + } From 4564b8446917e9d0057cda907fb9f2e59a4a3a48 Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Wed, 10 Dec 2025 17:30:23 +0100 Subject: [PATCH 469/701] [DURACOM-428] implemented custom exception for orcid connector (cherry picked from commit e3187bf201b82bec7ab1506ea10f6444c75e9c83) --- .../orcid/Orcidv3SolrAuthorityImpl.java | 58 ++++++++++++------- .../external/OrcidConnectionException.java | 33 +++++++++++ .../dspace/external/OrcidRestConnector.java | 34 +++++------ .../impl/OrcidV3AuthorDataProvider.java | 51 ++++++++++------ .../org/dspace/authority/orcid/MockOrcid.java | 7 ++- 5 files changed, 124 insertions(+), 59 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java diff --git a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java index 494daa97734a..312a00c146af 100644 --- a/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java +++ b/dspace-api/src/main/java/org/dspace/authority/orcid/Orcidv3SolrAuthorityImpl.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -20,6 +21,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.authority.AuthorityValue; import org.dspace.authority.SolrAuthorityInterface; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.dspace.external.provider.orcid.xml.XMLtoBio; import org.dspace.orcid.model.factory.OrcidFactoryUtils; @@ -142,9 +144,15 @@ public Person getBio(String id) { return null; } initializeAccessToken(); - InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - XMLtoBio converter = new XMLtoBio(); - return converter.convertSinglePerson(bioDocument); + try { + InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), + accessToken); + XMLtoBio converter = new XMLtoBio(); + return converter.convertSinglePerson(bioDocument); + } catch (OrcidConnectionException e) { + log.error("Error retrieving ORCID bio for ID=" + id, e); + return null; + } } @@ -167,29 +175,35 @@ public List queryBio(String text, int start, int rows) { // Check / init access token initializeAccessToken(); - String searchPath = "search?q=" + URLEncoder.encode(text) + "&start=" + start + "&rows=" + rows; + String searchPath = "search?q=" + URLEncoder.encode(text, StandardCharsets.UTF_8) + "&start=" + start + + "&rows=" + rows; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - XMLtoBio converter = new XMLtoBio(); - List results = converter.convert(bioDocument); - List bios = new LinkedList<>(); - for (Result result : results) { - OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); - if (orcidIdentifier != null) { - log.debug("Found OrcidId=" + orcidIdentifier.toString()); - String orcid = orcidIdentifier.getPath(); - Person bio = getBio(orcid); - if (bio != null) { - bios.add(bio); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + XMLtoBio converter = new XMLtoBio(); + List results = converter.convert(bioDocument); + List bios = new LinkedList<>(); + for (Result result : results) { + OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); + if (orcidIdentifier != null) { + log.debug("Found OrcidId=" + orcidIdentifier); + String orcid = orcidIdentifier.getPath(); + Person bio = getBio(orcid); + if (bio != null) { + bios.add(bio); + } } } + try { + bioDocument.close(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return bios; + } catch (OrcidConnectionException e) { + log.error("Error searching ORCID for query=" + text, e); + return Collections.emptyList(); } - try { - bioDocument.close(); - } catch (IOException e) { - log.error(e.getMessage(), e); - } - return bios; } /** diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java b/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java new file mode 100644 index 000000000000..3574045aab2b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/external/OrcidConnectionException.java @@ -0,0 +1,33 @@ +/** + * 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.external; + +/** + * Exception thrown when there are issues with ORCID service connections. + * + * @author Boychuk Mykhaylo (mykhaylo.boychuk@4science.com) + */ +public class OrcidConnectionException extends Exception { + + private final int statusCode; + + public OrcidConnectionException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + public OrcidConnectionException(String message, int statusCode, Throwable cause) { + super(message, cause); + this.statusCode = statusCode; + } + + public int getStatusCode() { + return statusCode; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java index 1e9688aa4ce6..3d61462cc354 100644 --- a/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OrcidRestConnector.java @@ -15,7 +15,6 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -36,7 +35,7 @@ public OrcidRestConnector(String url) { this.url = url; } - public InputStream get(String path, String accessToken) { + public InputStream get(String path, String accessToken) throws OrcidConnectionException { String fullPath = url + '/' + trimSlashes(path); HttpGet httpGet = new HttpGet(fullPath); if (StringUtils.isNotBlank(accessToken)) { @@ -44,23 +43,24 @@ public InputStream get(String path, String accessToken) { httpGet.addHeader("Authorization","Bearer " + accessToken); } try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().build()) { - CloseableHttpResponse httpResponse = httpClient.execute(httpGet); - if (!isSuccessful(httpResponse)) { - // Consume entity to avoid memory leak - EntityUtils.consumeQuietly(httpResponse.getEntity()); - var statusCode = getStatusCode(httpResponse); - var reason = httpResponse.getStatusLine().getReasonPhrase(); - var error = String.format("The request failed with:%d code, reason:%s ", statusCode, reason); - throw new RuntimeException(error); - } - try (InputStream responseStream = httpResponse.getEntity().getContent()) { - // Read all the content of the response stream into a byte array to prevent TruncatedChunkException - byte[] content = responseStream.readAllBytes(); - return new ByteArrayInputStream(content); + try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) { + if (!isSuccessful(httpResponse)) { + var statusCode = getStatusCode(httpResponse); + var reason = httpResponse.getStatusLine().getReasonPhrase(); + var error = String.format("The request failed with:%d code, reason:%s ", statusCode, reason); + throw new OrcidConnectionException(error, statusCode); + } + try (InputStream responseStream = httpResponse.getEntity().getContent()) { + // Read all the content of the response stream into a byte array to prevent TruncatedChunkException + byte[] content = responseStream.readAllBytes(); + return new ByteArrayInputStream(content); + } } + } catch (OrcidConnectionException e) { + throw e; } catch (Exception e) { log.error("Error in rest connector for path: " + fullPath, e); - throw new RuntimeException("Failed to execute ORCID request: " + fullPath, e); + throw new OrcidConnectionException("Failed to execute ORCID request: " + fullPath, 0, e); } } @@ -83,4 +83,4 @@ private int getStatusCode(HttpResponse response) { return response.getStatusLine().getStatusCode(); } -} +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index fad0e75b4fb7..fe2fdfa95381 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -21,6 +21,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.AbstractExternalDataProvider; @@ -171,8 +172,14 @@ public Person getBio(String id) { return null; } initializeAccessToken(); - InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - return converter.convertSinglePerson(bioDocument); + try { + InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), + accessToken); + return converter.convertSinglePerson(bioDocument); + } catch (OrcidConnectionException e) { + log.error("Error retrieving ORCID bio for ID=" + id, e); + return null; + } } /** @@ -203,21 +210,26 @@ public List searchExternalDataObjects(String query, int star + "&start=" + start + "&rows=" + limit; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - List results = converter.convert(bioDocument); - List bios = new LinkedList<>(); - for (Result result : results) { - OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); - if (orcidIdentifier != null) { - log.debug("Found OrcidId=" + orcidIdentifier.getPath()); - String orcid = orcidIdentifier.getPath(); - Person bio = getBio(orcid); - if (bio != null) { - bios.add(bio); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + List results = converter.convert(bioDocument); + List bios = new LinkedList<>(); + for (Result result : results) { + OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); + if (orcidIdentifier != null) { + log.debug("Found OrcidId=" + orcidIdentifier.getPath()); + String orcid = orcidIdentifier.getPath(); + Person bio = getBio(orcid); + if (bio != null) { + bios.add(bio); + } } } + return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); + } catch (OrcidConnectionException e) { + log.error("Error searching ORCID for query=" + query, e); + return Collections.emptyList(); } - return bios.stream().map(bio -> convertToExternalDataObject(bio)).collect(Collectors.toList()); } @Override @@ -236,8 +248,13 @@ public int getNumberOfResults(String query) { + "&start=" + 0 + "&rows=" + 0; log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); - InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); - return Math.min(converter.getNumberOfResultsFromXml(bioDocument), MAX_INDEX); + try { + InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); + return Math.min(converter.getNumberOfResultsFromXml(bioDocument), MAX_INDEX); + } catch (OrcidConnectionException e) { + log.error("Error getting number of results from ORCID for query=" + query, e); + return 0; + } } @@ -299,4 +316,4 @@ public void setOrcidRestConnector(OrcidRestConnector orcidRestConnector) { this.orcidRestConnector = orcidRestConnector; } -} +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java index 511df79f1e50..b6be6d1f3ac2 100644 --- a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java +++ b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java @@ -11,6 +11,7 @@ import java.io.InputStream; +import org.dspace.external.OrcidConnectionException; import org.dspace.external.OrcidRestConnector; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -38,7 +39,7 @@ public void init() { * Call this to set up mocking for any test classes that need it. We don't set it in init() * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing */ - public void setupNoResultsSearch() { + public void setupNoResultsSearch() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override @@ -51,7 +52,7 @@ public InputStream answer(InvocationOnMock invocation) { * Call this to set up mocking for any test classes that need it. We don't set it in init() * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing */ - public void setupSingleSearch() { + public void setupSingleSearch() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.startsWith("search?q=Bollini"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override @@ -64,7 +65,7 @@ public InputStream answer(InvocationOnMock invocation) { * Call this to set up mocking for any test classes that need it. We don't set it in init() * or other AbstractIntegrationTest implementations will complain of unnecessary Mockito stubbing */ - public void setupSearchWithResults() { + public void setupSearchWithResults() throws OrcidConnectionException { when(orcidRestConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) .thenAnswer(new Answer() { @Override From 01e66eed48ccdb35b6be0fcbc3b85e4844bd7e7c Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 28 Nov 2025 17:45:01 +0100 Subject: [PATCH 470/701] fix(authentication-method): Sets the authentication method just after the login has been concluded successfully ref: DURACOM-401 (cherry picked from commit 008998bee45e76ea5e3180dd8ec6c339b88ede51) --- .../java/org/dspace/authenticate/AuthenticationServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java index 493e4ade4168..4ad7fbd8c342 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java @@ -111,6 +111,7 @@ protected int authenticateInternal(Context context, } if (ret == AuthenticationMethod.SUCCESS) { updateLastActiveDate(context); + context.setAuthenticationMethod(aMethodStack.getName()); return ret; } if (ret < bestRet) { From 9271a2550b4128d7238c2c7334659b41dad7d8db Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 28 Nov 2025 17:51:54 +0100 Subject: [PATCH 471/701] fix(shib-auth-groups): Fixes special-groups check inside AuthenticationMethod.java Adds some IT to verify the correctness of the authentication. ref: DURACOM-401 (cherry picked from commit cf8e7f3c43697b82a45c05d3cfc9c0c8d50ec678) --- .../authenticate/AuthenticationMethod.java | 2 +- .../rest/AuthenticationRestControllerIT.java | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java index 38488c41e302..2273275f5d20 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java @@ -166,7 +166,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) * otherwise */ public default boolean areSpecialGroupsApplicable(Context context, HttpServletRequest request) { - return getName().equals(context.getAuthenticationMethod()); + return getName().equals(context.getAuthenticationMethod()) || isUsed(context,request); } /** diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index 1da807ad7180..988e9065ab46 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -1804,6 +1804,102 @@ private boolean tokenClaimsEqual(String token1, String token2) { } } + @Test + public void testShibbolethStaffMappedToStaffAndMembers() throws Exception { + context.turnOffAuthorisationSystem(); + + GroupBuilder.createGroup(context) + .withName("Staff") + .build(); + GroupBuilder.createGroup(context) + .withName("Member") + .build(); + + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); + configurationService.setProperty("authentication-shibboleth.role.staff", "Staff, Member"); + configurationService.setProperty("authentication-shibboleth.default-roles", "staff"); + configurationService.setProperty("authentication-shibboleth.netid-header", "mail"); + configurationService.setProperty("authentication-shibboleth.email-header", "mail"); + + context.restoreAuthSystemState(); + + String shibToken = getClient().perform(post("/api/authn/login") + .requestAttr("mail", eperson.getEmail()) + .requestAttr("SHIB-SCOPED-AFFILIATION", "staff")) + .andExpect(status().isOk()) + .andReturn().getResponse().getHeader(AUTHORIZATION_HEADER).replace(AUTHORIZATION_TYPE, ""); + + getClient(shibToken).perform(get("/api/authn/status").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.authenticationMethod", is("shibboleth"))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("Staff"), + matchGroupWithName("Member") + ) + )); + + getClient(shibToken).perform(get("/api/authn/status/specialGroups").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("Staff"), + matchGroupWithName("Member") + ) + )); + } + + @Test + public void testPasswordLoginNotMappedToStaffAndMembers() throws Exception { + context.turnOffAuthorisationSystem(); + + GroupBuilder.createGroup(context) + .withName("Staff") + .build(); + GroupBuilder.createGroup(context) + .withName("Member") + .build(); + GroupBuilder.createGroup(context) + .withName("specialGroupPwd") + .build(); + + + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", + "org.dspace.authenticate.PasswordAuthentication, org.dspace.authenticate.ShibAuthentication"); + configurationService.setProperty("authentication-shibboleth.role.staff", "Staff, Member"); + configurationService.setProperty("authentication-shibboleth.default-roles", "staff"); + configurationService.setProperty("authentication-shibboleth.netid-header", "mail"); + configurationService.setProperty("authentication-shibboleth.email-header", "mail"); + configurationService.setProperty("authentication-password.login.specialgroup", "specialGroupPwd"); + + context.restoreAuthSystemState(); + + String passwordToken = getAuthToken(eperson.getEmail(), password); + + getClient(passwordToken).perform(get("/api/authn/status").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.authenticationMethod", is("password"))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("specialGroupPwd") + ) + )); + + getClient(passwordToken).perform(get("/api/authn/status/specialGroups").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("specialGroupPwd") + ) + )); + } + + + private OrcidTokenResponseDTO buildOrcidTokenResponse(String orcid, String accessToken) { OrcidTokenResponseDTO token = new OrcidTokenResponseDTO(); token.setAccessToken(accessToken); From 0910df2113bfb61c64e48fe4aa7671e260efa2bc Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 15 Dec 2025 13:06:52 +0100 Subject: [PATCH 472/701] fix(style): Adds missing space between parameters of isUsed method ref: DURACOM-401 (cherry picked from commit 40cc61b493e0bec624416fe8aa6f9744d142b6ff) --- .../main/java/org/dspace/authenticate/AuthenticationMethod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java index 2273275f5d20..7c8793a6c21a 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java @@ -166,7 +166,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) * otherwise */ public default boolean areSpecialGroupsApplicable(Context context, HttpServletRequest request) { - return getName().equals(context.getAuthenticationMethod()) || isUsed(context,request); + return getName().equals(context.getAuthenticationMethod()) || isUsed(context, request); } /** From 95a1ec30ac45bc6db64df4bf92b695b1865d4e39 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 28 Nov 2025 17:45:01 +0100 Subject: [PATCH 473/701] fix(authentication-method): Sets the authentication method just after the login has been concluded successfully ref: DURACOM-401 (cherry picked from commit 008998bee45e76ea5e3180dd8ec6c339b88ede51) --- .../java/org/dspace/authenticate/AuthenticationServiceImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java index 9387faeb738e..c0dde49b13a3 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java @@ -110,6 +110,7 @@ protected int authenticateInternal(Context context, } if (ret == AuthenticationMethod.SUCCESS) { updateLastActiveDate(context); + context.setAuthenticationMethod(aMethodStack.getName()); return ret; } if (ret < bestRet) { From 2b70d3a56e7268c9bc45f0a02fdfec9553e28931 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 28 Nov 2025 17:51:54 +0100 Subject: [PATCH 474/701] fix(shib-auth-groups): Fixes special-groups check inside AuthenticationMethod.java Adds some IT to verify the correctness of the authentication. ref: DURACOM-401 (cherry picked from commit cf8e7f3c43697b82a45c05d3cfc9c0c8d50ec678) --- .../authenticate/AuthenticationMethod.java | 2 +- .../rest/AuthenticationRestControllerIT.java | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java index 9f1c0827e979..e83d20aae890 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java @@ -166,7 +166,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) * otherwise */ public default boolean areSpecialGroupsApplicable(Context context, HttpServletRequest request) { - return getName().equals(context.getAuthenticationMethod()); + return getName().equals(context.getAuthenticationMethod()) || isUsed(context,request); } /** diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index 9edb0a2a9f40..b4c3e720cfc6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -1803,6 +1803,102 @@ private boolean tokenClaimsEqual(String token1, String token2) { } } + @Test + public void testShibbolethStaffMappedToStaffAndMembers() throws Exception { + context.turnOffAuthorisationSystem(); + + GroupBuilder.createGroup(context) + .withName("Staff") + .build(); + GroupBuilder.createGroup(context) + .withName("Member") + .build(); + + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); + configurationService.setProperty("authentication-shibboleth.role.staff", "Staff, Member"); + configurationService.setProperty("authentication-shibboleth.default-roles", "staff"); + configurationService.setProperty("authentication-shibboleth.netid-header", "mail"); + configurationService.setProperty("authentication-shibboleth.email-header", "mail"); + + context.restoreAuthSystemState(); + + String shibToken = getClient().perform(post("/api/authn/login") + .requestAttr("mail", eperson.getEmail()) + .requestAttr("SHIB-SCOPED-AFFILIATION", "staff")) + .andExpect(status().isOk()) + .andReturn().getResponse().getHeader(AUTHORIZATION_HEADER).replace(AUTHORIZATION_TYPE, ""); + + getClient(shibToken).perform(get("/api/authn/status").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.authenticationMethod", is("shibboleth"))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("Staff"), + matchGroupWithName("Member") + ) + )); + + getClient(shibToken).perform(get("/api/authn/status/specialGroups").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("Staff"), + matchGroupWithName("Member") + ) + )); + } + + @Test + public void testPasswordLoginNotMappedToStaffAndMembers() throws Exception { + context.turnOffAuthorisationSystem(); + + GroupBuilder.createGroup(context) + .withName("Staff") + .build(); + GroupBuilder.createGroup(context) + .withName("Member") + .build(); + GroupBuilder.createGroup(context) + .withName("specialGroupPwd") + .build(); + + + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", + "org.dspace.authenticate.PasswordAuthentication, org.dspace.authenticate.ShibAuthentication"); + configurationService.setProperty("authentication-shibboleth.role.staff", "Staff, Member"); + configurationService.setProperty("authentication-shibboleth.default-roles", "staff"); + configurationService.setProperty("authentication-shibboleth.netid-header", "mail"); + configurationService.setProperty("authentication-shibboleth.email-header", "mail"); + configurationService.setProperty("authentication-password.login.specialgroup", "specialGroupPwd"); + + context.restoreAuthSystemState(); + + String passwordToken = getAuthToken(eperson.getEmail(), password); + + getClient(passwordToken).perform(get("/api/authn/status").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.authenticationMethod", is("password"))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("specialGroupPwd") + ) + )); + + getClient(passwordToken).perform(get("/api/authn/status/specialGroups").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + matchGroupWithName("specialGroupPwd") + ) + )); + } + + + private OrcidTokenResponseDTO buildOrcidTokenResponse(String orcid, String accessToken) { OrcidTokenResponseDTO token = new OrcidTokenResponseDTO(); token.setAccessToken(accessToken); From 0ba5a3389024cb310608148805ac42112136b797 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 15 Dec 2025 13:06:52 +0100 Subject: [PATCH 475/701] fix(style): Adds missing space between parameters of isUsed method ref: DURACOM-401 (cherry picked from commit 40cc61b493e0bec624416fe8aa6f9744d142b6ff) --- .../main/java/org/dspace/authenticate/AuthenticationMethod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java index e83d20aae890..a7140f244dfa 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationMethod.java @@ -166,7 +166,7 @@ public List getSpecialGroups(Context context, HttpServletRequest request) * otherwise */ public default boolean areSpecialGroupsApplicable(Context context, HttpServletRequest request) { - return getName().equals(context.getAuthenticationMethod()) || isUsed(context,request); + return getName().equals(context.getAuthenticationMethod()) || isUsed(context, request); } /** From 34dfdd502fcf48cee5b306fc33a0881953449581 Mon Sep 17 00:00:00 2001 From: Daniel Coelho Date: Fri, 19 Sep 2025 15:53:03 -0300 Subject: [PATCH 476/701] Trigger object reindex when updating eperson or eperson group of an associated resource policy (fixes #11325) (cherry picked from commit fbddb06ae2377b8f4475df27c6b371fa5358fb45) --- .../app/rest/ResourcePolicyEPersonReplaceRestController.java | 1 + .../app/rest/ResourcePolicyGroupReplaceRestController.java | 1 + 2 files changed, 2 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java index e772aa0abe18..e625644c610b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java @@ -75,6 +75,7 @@ public ResponseEntity> replaceEPersonOfResourcePolicy(@Pa } EPerson newEPerson = (EPerson) dsoList.get(0); resourcePolicy.setEPerson(newEPerson); + resourcePolicyService.update(context, resourcePolicy); context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java index e9ba0dff4429..c6927df8150e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java @@ -75,6 +75,7 @@ public ResponseEntity> replaceGroupOfResourcePolicy(@Path Group newGroup = (Group) dsoList.get(0); resourcePolicy.setGroup(newGroup); + resourcePolicyService.update(context, resourcePolicy); context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } From c4a1a6378af9497de22bfdf0d165c760896fec74 Mon Sep 17 00:00:00 2001 From: Daniel Coelho Date: Fri, 19 Sep 2025 15:53:03 -0300 Subject: [PATCH 477/701] Trigger object reindex when updating eperson or eperson group of an associated resource policy (fixes #11325) (cherry picked from commit fbddb06ae2377b8f4475df27c6b371fa5358fb45) --- .../app/rest/ResourcePolicyEPersonReplaceRestController.java | 1 + .../app/rest/ResourcePolicyGroupReplaceRestController.java | 1 + 2 files changed, 2 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java index 3311f303ade6..b8b638c82ae4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java @@ -75,6 +75,7 @@ public ResponseEntity> replaceEPersonOfResourcePolicy(@Pa } EPerson newEPerson = (EPerson) dsoList.get(0); resourcePolicy.setEPerson(newEPerson); + resourcePolicyService.update(context, resourcePolicy); context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java index 2a041aba3a0a..6f08cf2cf99a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java @@ -75,6 +75,7 @@ public ResponseEntity> replaceGroupOfResourcePolicy(@Path Group newGroup = (Group) dsoList.get(0); resourcePolicy.setGroup(newGroup); + resourcePolicyService.update(context, resourcePolicy); context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } From e388b83c7d542de5abcaca9685bdd6a6a9b06214 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 16 Dec 2025 15:03:41 -0600 Subject: [PATCH 478/701] Update LICENSES_THIRD_PARTY for v7.6.6 --- LICENSES_THIRD_PARTY | 314 +++++++++++++++++++++++-------------------- 1 file changed, 165 insertions(+), 149 deletions(-) diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index 92868467c989..174c8760cab0 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -21,19 +21,14 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Apache Software License, Version 2.0: * Ant-Contrib Tasks (ant-contrib:ant-contrib:1.0b3 - http://ant-contrib.sourceforge.net) - * AWS SDK for Java - Core (com.amazonaws:aws-java-sdk-core:1.12.785 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK for AWS KMS (com.amazonaws:aws-java-sdk-kms:1.12.785 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK for Amazon S3 (com.amazonaws:aws-java-sdk-s3:1.12.785 - https://aws.amazon.com/sdkforjava) - * JMES Path Query library (com.amazonaws:jmespath-java:1.12.785 - https://aws.amazon.com/sdkforjava) * HPPC Collections (com.carrotsearch:hppc:0.8.1 - http://labs.carrotsearch.com/hppc.html/hppc) * com.drewnoakes:metadata-extractor (com.drewnoakes:metadata-extractor:2.19.0 - https://drewnoakes.com/code/exif/) * parso (com.epam:parso:2.0.14 - https://github.com/epam/parso) * Internet Time Utility (com.ethlo.time:itu:1.7.0 - https://github.com/ethlo/itu) - * ClassMate (com.fasterxml:classmate:1.7.0 - https://github.com/FasterXML/java-classmate) - * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.19.1 - https://github.com/FasterXML/jackson) - * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.19.1 - https://github.com/FasterXML/jackson-core) - * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.19.1 - https://github.com/FasterXML/jackson) - * Jackson dataformat: CBOR (com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.17.2 - https://github.com/FasterXML/jackson-dataformats-binary) + * ClassMate (com.fasterxml:classmate:1.7.1 - https://github.com/FasterXML/java-classmate) + * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.20 - https://github.com/FasterXML/jackson) + * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.20.1 - https://github.com/FasterXML/jackson-core) + * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.20.1 - https://github.com/FasterXML/jackson) * Jackson dataformat: Smile (com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.15.2 - https://github.com/FasterXML/jackson-dataformats-binary) * Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.14.0 - https://github.com/FasterXML/jackson-dataformats-text) * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.5 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) @@ -45,6 +40,9 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * zjsonpatch (com.flipkart.zjsonpatch:zjsonpatch:0.4.16 - https://github.com/flipkart-incubator/zjsonpatch/) * Caffeine cache (com.github.ben-manes.caffeine:caffeine:2.9.3 - https://github.com/ben-manes/caffeine) * JSON.simple (com.github.cliftonlabs:json-simple:3.0.2 - https://cliftonlabs.github.io/json-simple/) + * docker-java-api (com.github.docker-java:docker-java-api:3.7.0 - https://github.com/docker-java/docker-java) + * docker-java-transport (com.github.docker-java:docker-java-transport:3.7.0 - https://github.com/docker-java/docker-java) + * docker-java-transport-zerodep (com.github.docker-java:docker-java-transport-zerodep:3.7.0 - https://github.com/docker-java/docker-java) * btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) * jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) * jackson-coreutils-equivalence (com.github.java-json-tools:jackson-coreutils-equivalence:1.0 - https://github.com/java-json-tools/jackson-coreutils) @@ -62,26 +60,26 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess) * Guava: Google Core Libraries for Java (com.google.guava:guava:32.1.3-jre - https://github.com/google/guava) * Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) - * Google HTTP Client Library for Java (com.google.http-client:google-http-client:1.47.0 - https://github.com/googleapis/google-http-java-client/google-http-client) + * Google HTTP Client Library for Java (com.google.http-client:google-http-client:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client) * Apache HTTP transport v2 for the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-apache-v2:1.42.0 - https://github.com/googleapis/google-http-java-client/google-http-client-apache-v2) - * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.47.0 - https://github.com/googleapis/google-http-java-client/google-http-client-gson) - * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.47.0 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson2) + * GSON extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-gson:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client-gson) + * Jackson 2 extensions to the Google HTTP Client Library for Java. (com.google.http-client:google-http-client-jackson2:1.47.1 - https://github.com/googleapis/google-http-java-client/google-http-client-jackson2) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:2.8 - https://github.com/google/j2objc/) * Google OAuth Client Library for Java (com.google.oauth-client:google-oauth-client:1.39.0 - https://github.com/googleapis/google-oauth-java-client/google-oauth-client) * ConcurrentLinkedHashMap (com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2 - http://code.google.com/p/concurrentlinkedhashmap) * libphonenumber (com.googlecode.libphonenumber:libphonenumber:8.11.1 - https://github.com/google/libphonenumber/) - * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.8 - https://jackcess.sourceforge.io) + * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.10 - https://jackcess.sourceforge.io) * Jackcess Encrypt (com.healthmarketscience.jackcess:jackcess-encrypt:4.0.3 - http://jackcessencrypt.sf.net) - * json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) - * json-path-assert (com.jayway.jsonpath:json-path-assert:2.9.0 - https://github.com/jayway/JsonPath) + * json-path (com.jayway.jsonpath:json-path:2.10.0 - https://github.com/jayway/JsonPath) + * json-path-assert (com.jayway.jsonpath:json-path-assert:2.10.0 - https://github.com/jayway/JsonPath) * Disruptor Framework (com.lmax:disruptor:3.4.2 - http://lmax-exchange.github.com/disruptor) * MaxMind DB Reader (com.maxmind.db:maxmind-db:2.1.0 - http://dev.maxmind.com/) * MaxMind GeoIP2 API (com.maxmind.geoip2:geoip2:2.17.0 - https://dev.maxmind.com/geoip?lang=en) * JsonSchemaValidator (com.networknt:json-schema-validator:1.0.76 - https://github.com/networknt/json-schema-validator) * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:7.9 - https://bitbucket.org/connect2id/nimbus-jose-jwt) * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.28 - https://bitbucket.org/connect2id/nimbus-jose-jwt) - * opencsv (com.opencsv:opencsv:5.11.1 - http://opencsv.sf.net) + * opencsv (com.opencsv:opencsv:5.12.0 - http://opencsv.sf.net) * java-libpst (com.pff:java-libpst:0.9.3 - https://github.com/rjohnsondev/java-libpst) * rome (com.rometools:rome:1.19.0 - http://rometools.com/rome) * rome-modules (com.rometools:rome-modules:1.19.0 - http://rometools.com/rome-modules) @@ -91,27 +89,18 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * okio (com.squareup.okio:okio:3.6.0 - https://github.com/square/okio/) * okio (com.squareup.okio:okio-jvm:3.6.0 - https://github.com/square/okio/) * T-Digest (com.tdunning:t-digest:3.1 - https://github.com/tdunning/t-digest) - * config (com.typesafe:config:1.3.3 - https://github.com/lightbend/config) - * ssl-config-core (com.typesafe:ssl-config-core_2.13:0.3.8 - https://github.com/lightbend/ssl-config) - * akka-actor (com.typesafe.akka:akka-actor_2.13:2.5.31 - https://akka.io/) - * akka-http-core (com.typesafe.akka:akka-http-core_2.13:10.1.12 - https://akka.io) - * akka-http (com.typesafe.akka:akka-http_2.13:10.1.12 - https://akka.io) - * akka-parsing (com.typesafe.akka:akka-parsing_2.13:10.1.12 - https://akka.io) - * akka-protobuf (com.typesafe.akka:akka-protobuf_2.13:2.5.31 - https://akka.io/) - * akka-stream (com.typesafe.akka:akka-stream_2.13:2.5.31 - https://akka.io/) - * scala-logging (com.typesafe.scala-logging:scala-logging_2.13:3.9.2 - https://github.com/lightbend/scala-logging) * JSON library from Android SDK (com.vaadin.external.google:android-json:0.0.20131108.vaadin1 - http://developer.android.com/sdk) * SparseBitSet (com.zaxxer:SparseBitSet:1.3 - https://github.com/brettwooldridge/SparseBitSet) * Apache Commons BeanUtils (commons-beanutils:commons-beanutils:1.11.0 - https://commons.apache.org/proper/commons-beanutils) - * Apache Commons CLI (commons-cli:commons-cli:1.9.0 - https://commons.apache.org/proper/commons-cli/) - * Apache Commons Codec (commons-codec:commons-codec:1.18.0 - https://commons.apache.org/proper/commons-codec/) + * Apache Commons CLI (commons-cli:commons-cli:1.11.0 - https://commons.apache.org/proper/commons-cli/) + * Apache Commons Codec (commons-codec:commons-codec:1.20.0 - https://commons.apache.org/proper/commons-codec/) * Apache Commons Collections (commons-collections:commons-collections:3.2.2 - http://commons.apache.org/collections/) * Commons Digester (commons-digester:commons-digester:2.1 - http://commons.apache.org/digester/) * Commons FileUpload (commons-fileupload:commons-fileupload:1.2.1 - http://commons.apache.org/fileupload/) - * Apache Commons IO (commons-io:commons-io:2.19.0 - https://commons.apache.org/proper/commons-io/) + * Apache Commons IO (commons-io:commons-io:2.21.0 - https://commons.apache.org/proper/commons-io/) * Commons Lang (commons-lang:commons-lang:2.6 - http://commons.apache.org/lang/) * Apache Commons Logging (commons-logging:commons-logging:1.3.5 - https://commons.apache.org/proper/commons-logging/) - * Apache Commons Validator (commons-validator:commons-validator:1.9.0 - http://commons.apache.org/proper/commons-validator/) + * Apache Commons Validator (commons-validator:commons-validator:1.10.1 - https://commons.apache.org/proper/commons-validator/) * GeoJson POJOs for Jackson (de.grundid.opendatalab:geojson-jackson:1.14 - https://github.com/opendatalab-de/geojson-jackson) * OpenAIRE Funders Model (eu.openaire:funders-model:2.0.0 - https://api.openaire.eu) * Metrics Core (io.dropwizard.metrics:metrics-core:4.1.5 - https://metrics.dropwizard.io/metrics-core) @@ -119,34 +108,34 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Metrics Integration for Jetty 9.3 and higher (io.dropwizard.metrics:metrics-jetty9:4.1.5 - https://metrics.dropwizard.io/metrics-jetty9) * Metrics Integration with JMX (io.dropwizard.metrics:metrics-jmx:4.1.5 - https://metrics.dropwizard.io/metrics-jmx) * JVM Integration for Metrics (io.dropwizard.metrics:metrics-jvm:4.1.5 - https://metrics.dropwizard.io/metrics-jvm) - * io.grpc:grpc-api (io.grpc:grpc-api:1.73.0 - https://github.com/grpc/grpc-java) - * io.grpc:grpc-context (io.grpc:grpc-context:1.73.0 - https://github.com/grpc/grpc-java) + * io.grpc:grpc-api (io.grpc:grpc-api:1.77.0 - https://github.com/grpc/grpc-java) + * io.grpc:grpc-context (io.grpc:grpc-context:1.77.0 - https://github.com/grpc/grpc-java) * micrometer-core (io.micrometer:micrometer-core:1.9.17 - https://github.com/micrometer-metrics/micrometer) * Netty/Buffer (io.netty:netty-buffer:4.1.99.Final - https://netty.io/netty-buffer/) - * Netty/Buffer (io.netty:netty-buffer:4.2.2.Final - https://netty.io/netty-buffer/) + * Netty/Buffer (io.netty:netty-buffer:4.2.7.Final - https://netty.io/netty-buffer/) * Netty/Codec (io.netty:netty-codec:4.1.99.Final - https://netty.io/netty-codec/) - * Netty/Codec (io.netty:netty-codec:4.2.2.Final - https://netty.io/netty-codec/) - * Netty/Codec/Base (io.netty:netty-codec-base:4.2.2.Final - https://netty.io/netty-codec-base/) - * Netty/Codec/Compression (io.netty:netty-codec-compression:4.2.2.Final - https://netty.io/netty-codec-compression/) + * Netty/Codec (io.netty:netty-codec:4.2.7.Final - https://netty.io/netty-codec/) + * Netty/Codec/Base (io.netty:netty-codec-base:4.2.7.Final - https://netty.io/netty-codec-base/) + * Netty/Codec/Compression (io.netty:netty-codec-compression:4.2.7.Final - https://netty.io/netty-codec-compression/) * Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.86.Final - https://netty.io/netty-codec-http/) * Netty/Codec/HTTP2 (io.netty:netty-codec-http2:4.1.86.Final - https://netty.io/netty-codec-http2/) - * Netty/Codec/Marshalling (io.netty:netty-codec-marshalling:4.2.2.Final - https://netty.io/netty-codec-marshalling/) - * Netty/Codec/Protobuf (io.netty:netty-codec-protobuf:4.2.2.Final - https://netty.io/netty-codec-protobuf/) + * Netty/Codec/Marshalling (io.netty:netty-codec-marshalling:4.2.7.Final - https://netty.io/netty-codec-marshalling/) + * Netty/Codec/Protobuf (io.netty:netty-codec-protobuf:4.2.7.Final - https://netty.io/netty-codec-protobuf/) * Netty/Codec/Socks (io.netty:netty-codec-socks:4.1.86.Final - https://netty.io/netty-codec-socks/) * Netty/Common (io.netty:netty-common:4.1.99.Final - https://netty.io/netty-common/) - * Netty/Common (io.netty:netty-common:4.2.2.Final - https://netty.io/netty-common/) + * Netty/Common (io.netty:netty-common:4.2.7.Final - https://netty.io/netty-common/) * Netty/Handler (io.netty:netty-handler:4.1.99.Final - https://netty.io/netty-handler/) - * Netty/Handler (io.netty:netty-handler:4.2.2.Final - https://netty.io/netty-handler/) + * Netty/Handler (io.netty:netty-handler:4.2.7.Final - https://netty.io/netty-handler/) * Netty/Handler/Proxy (io.netty:netty-handler-proxy:4.1.86.Final - https://netty.io/netty-handler-proxy/) * Netty/Resolver (io.netty:netty-resolver:4.1.99.Final - https://netty.io/netty-resolver/) * Netty/TomcatNative [BoringSSL - Static] (io.netty:netty-tcnative-boringssl-static:2.0.56.Final - https://github.com/netty/netty-tcnative/netty-tcnative-boringssl-static/) * Netty/TomcatNative [OpenSSL - Classes] (io.netty:netty-tcnative-classes:2.0.56.Final - https://github.com/netty/netty-tcnative/netty-tcnative-classes/) * Netty/Transport (io.netty:netty-transport:4.1.99.Final - https://netty.io/netty-transport/) - * Netty/Transport (io.netty:netty-transport:4.2.2.Final - https://netty.io/netty-transport/) + * Netty/Transport (io.netty:netty-transport:4.2.7.Final - https://netty.io/netty-transport/) * Netty/Transport/Classes/Epoll (io.netty:netty-transport-classes-epoll:4.1.99.Final - https://netty.io/netty-transport-classes-epoll/) * Netty/Transport/Native/Epoll (io.netty:netty-transport-native-epoll:4.1.99.Final - https://netty.io/netty-transport-native-epoll/) * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.1.99.Final - https://netty.io/netty-transport-native-unix-common/) - * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.2.2.Final - https://netty.io/netty-transport-native-unix-common/) + * Netty/Transport/Native/Unix/Common (io.netty:netty-transport-native-unix-common:4.2.7.Final - https://netty.io/netty-transport-native-unix-common/) * OpenCensus (io.opencensus:opencensus-api:0.31.1 - https://github.com/census-instrumentation/opencensus-java) * OpenCensus (io.opencensus:opencensus-contrib-http-util:0.31.1 - https://github.com/census-instrumentation/opencensus-java) * OpenTracing API (io.opentracing:opentracing-api:0.33.0 - https://github.com/opentracing/opentracing-java/opentracing-api) @@ -180,33 +169,32 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.12.18 - https://bytebuddy.net/byte-buddy) * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.11.13 - https://bytebuddy.net/byte-buddy-agent) * eigenbase-properties (net.hydromatic:eigenbase-properties:1.1.5 - http://github.com/julianhyde/eigenbase-properties) + * Java Native Access (net.java.dev.jna:jna:5.18.1 - https://github.com/java-native-access/jna) * json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.36.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) * "Java Concurrency in Practice" book annotations (net.jcip:jcip-annotations:1.0 - http://jcip.net/) - * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) - * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.2 - https://urielch.github.io/) - * JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) - * JSON Small and Fast Parser (net.minidev:json-smart:2.5.2 - https://urielch.github.io/) + * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.6.0 - https://urielch.github.io/) + * JSON Small and Fast Parser (net.minidev:json-smart:2.6.0 - https://urielch.github.io/) * Abdera Core (org.apache.abdera:abdera-core:1.1.3 - http://abdera.apache.org/abdera-core) * I18N Libraries (org.apache.abdera:abdera-i18n:1.1.3 - http://abdera.apache.org) * Apache Ant Core (org.apache.ant:ant:1.10.15 - https://ant.apache.org/) * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.15 - https://ant.apache.org/) - * Apache Commons BCEL (org.apache.bcel:bcel:6.10.0 - https://commons.apache.org/proper/commons-bcel) + * Apache Commons BCEL (org.apache.bcel:bcel:6.11.0 - https://commons.apache.org/proper/commons-bcel) * Calcite Core (org.apache.calcite:calcite-core:1.35.0 - https://calcite.apache.org) * Calcite Linq4j (org.apache.calcite:calcite-linq4j:1.35.0 - https://calcite.apache.org) * Apache Calcite Avatica (org.apache.calcite.avatica:avatica-core:1.23.0 - https://calcite.apache.org/avatica) * Apache Calcite Avatica Metrics (org.apache.calcite.avatica:avatica-metrics:1.23.0 - https://calcite.apache.org/avatica) * Apache Commons Collections (org.apache.commons:commons-collections4:4.5.0 - https://commons.apache.org/proper/commons-collections/) - * Apache Commons Compress (org.apache.commons:commons-compress:1.27.1 - https://commons.apache.org/proper/commons-compress/) - * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.12.0 - https://commons.apache.org/proper/commons-configuration/) - * Apache Commons CSV (org.apache.commons:commons-csv:1.14.0 - https://commons.apache.org/proper/commons-csv/) + * Apache Commons Compress (org.apache.commons:commons-compress:1.28.0 - https://commons.apache.org/proper/commons-compress/) + * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.13.0 - https://commons.apache.org/proper/commons-configuration/) + * Apache Commons CSV (org.apache.commons:commons-csv:1.14.1 - https://commons.apache.org/proper/commons-csv/) * Apache Commons DBCP (org.apache.commons:commons-dbcp2:2.13.0 - https://commons.apache.org/proper/commons-dbcp/) * Apache Commons Digester (org.apache.commons:commons-digester3:3.2 - http://commons.apache.org/digester/) * Apache Commons Exec (org.apache.commons:commons-exec:1.3 - http://commons.apache.org/proper/commons-exec/) - * Apache Commons Exec (org.apache.commons:commons-exec:1.4.0 - https://commons.apache.org/proper/commons-exec/) - * Apache Commons Lang (org.apache.commons:commons-lang3:3.17.0 - https://commons.apache.org/proper/commons-lang/) + * Apache Commons Exec (org.apache.commons:commons-exec:1.5.0 - https://commons.apache.org/proper/commons-exec/) + * Apache Commons Lang (org.apache.commons:commons-lang3:3.20.0 - https://commons.apache.org/proper/commons-lang/) * Apache Commons Math (org.apache.commons:commons-math3:3.6.1 - http://commons.apache.org/proper/commons-math/) * Apache Commons Pool (org.apache.commons:commons-pool2:2.12.1 - https://commons.apache.org/proper/commons-pool/) - * Apache Commons Text (org.apache.commons:commons-text:1.13.1 - https://commons.apache.org/proper/commons-text) + * Apache Commons Text (org.apache.commons:commons-text:1.14.0 - https://commons.apache.org/proper/commons-text) * Curator Client (org.apache.curator:curator-client:2.13.0 - http://curator.apache.org/curator-client) * Curator Framework (org.apache.curator:curator-framework:2.13.0 - http://curator.apache.org/curator-framework) * Curator Recipes (org.apache.curator:curator-recipes:2.13.0 - http://curator.apache.org/curator-recipes) @@ -222,8 +210,8 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.1.3 - https://hc.apache.org/httpcomponents-client-5.0.x/5.1.3/httpclient5/) * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5/) * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5-h2/) - * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.12 - http://james.apache.org/mime4j/apache-mime4j-core) - * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.12 - http://james.apache.org/mime4j/apache-mime4j-dom) + * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.13 - http://james.apache.org/mime4j/apache-mime4j-core) + * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.13 - http://james.apache.org/mime4j/apache-mime4j-dom) * Apache Jena - Libraries POM (org.apache.jena:apache-jena-libs:2.13.0 - http://jena.apache.org/apache-jena-libs/) * Apache Jena - ARQ (SPARQL 1.1 Query Engine) (org.apache.jena:jena-arq:2.13.0 - http://jena.apache.org/jena-arq/) * Apache Jena - Core (org.apache.jena:jena-core:2.13.0 - http://jena.apache.org/jena-core/) @@ -233,9 +221,9 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Kerby-kerb Util (org.apache.kerby:kerb-util:1.0.1 - http://directory.apache.org/kerby/kerby-kerb/kerb-util) * Kerby ASN1 Project (org.apache.kerby:kerby-asn1:1.0.1 - http://directory.apache.org/kerby/kerby-common/kerby-asn1) * Kerby PKIX Project (org.apache.kerby:kerby-pkix:1.0.1 - http://directory.apache.org/kerby/kerby-pkix) - * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.25.1 - https://logging.apache.org/log4j/2.x/) - * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.1 - https://logging.apache.org/log4j/2.x/) - * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.1 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.25.2 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.2 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.2 - https://logging.apache.org/log4j/2.x/) * Apache Log4j JUL Adapter (org.apache.logging.log4j:log4j-jul:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-jul/) * Apache Log4j Layout for JSON template (org.apache.logging.log4j:log4j-layout-template-json:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-layout-template-json/) * Apache Log4j SLF4J Binding (org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/) @@ -263,12 +251,13 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Lucene Spatial Extras (org.apache.lucene:lucene-spatial-extras:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial-extras) * Lucene Spatial 3D (org.apache.lucene:lucene-spatial3d:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial3d) * Lucene Suggest (org.apache.lucene:lucene-suggest:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-suggest) - * Apache FontBox (org.apache.pdfbox:fontbox:2.0.34 - http://pdfbox.apache.org/) + * Apache FontBox (org.apache.pdfbox:fontbox:3.0.5 - http://pdfbox.apache.org/) * PDFBox JBIG2 ImageIO plugin (org.apache.pdfbox:jbig2-imageio:3.0.4 - https://www.apache.org/jbig2-imageio/) * Apache JempBox (org.apache.pdfbox:jempbox:1.8.17 - http://www.apache.org/pdfbox-parent/jempbox/) - * Apache PDFBox (org.apache.pdfbox:pdfbox:2.0.34 - https://www.apache.org/pdfbox-parent/pdfbox/) - * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:2.0.34 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) - * Apache XmpBox (org.apache.pdfbox:xmpbox:2.0.34 - https://www.apache.org/pdfbox-parent/xmpbox/) + * Apache PDFBox (org.apache.pdfbox:pdfbox:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox/) + * Apache PDFBox io (org.apache.pdfbox:pdfbox-io:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox-io/) + * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) + * Apache XmpBox (org.apache.pdfbox:xmpbox:3.0.5 - https://www.apache.org/pdfbox-parent/xmpbox/) * Apache POI - Common (org.apache.poi:poi:5.4.1 - https://poi.apache.org/) * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.4.1 - https://poi.apache.org/) * Apache POI (org.apache.poi:poi-ooxml-lite:5.4.1 - https://poi.apache.org/) @@ -278,30 +267,30 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache Standard Taglib Implementation (org.apache.taglibs:taglibs-standard-impl:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-impl) * Apache Standard Taglib Specification API (org.apache.taglibs:taglibs-standard-spec:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-spec) * Apache Thrift (org.apache.thrift:libthrift:0.9.2 - http://thrift.apache.org) - * Apache Tika core (org.apache.tika:tika-core:2.9.4 - https://tika.apache.org/) - * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:2.9.4 - https://tika.apache.org/tika-parser-apple-module/) - * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:2.9.4 - https://tika.apache.org/tika-parser-audiovideo-module/) - * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:2.9.4 - https://tika.apache.org/tika-parser-cad-module/) - * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:2.9.4 - https://tika.apache.org/tika-parser-code-module/) - * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:2.9.4 - https://tika.apache.org/tika-parser-crypto-module/) - * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:2.9.4 - https://tika.apache.org/tika-parser-digest-commons/) - * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:2.9.4 - https://tika.apache.org/tika-parser-font-module/) - * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:2.9.4 - https://tika.apache.org/tika-parser-html-module/) - * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:2.9.4 - https://tika.apache.org/tika-parser-image-module/) - * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:2.9.4 - https://tika.apache.org/tika-parser-mail-commons/) - * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:2.9.4 - https://tika.apache.org/tika-parser-mail-module/) - * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:2.9.4 - https://tika.apache.org/tika-parser-microsoft-module/) - * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:2.9.4 - https://tika.apache.org/tika-parser-miscoffice-module/) - * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:2.9.4 - https://tika.apache.org/tika-parser-news-module/) - * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:2.9.4 - https://tika.apache.org/tika-parser-ocr-module/) - * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:2.9.4 - https://tika.apache.org/tika-parser-pdf-module/) - * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:2.9.4 - https://tika.apache.org/tika-parser-pkg-module/) - * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:2.9.4 - https://tika.apache.org/tika-parser-text-module/) - * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:2.9.4 - https://tika.apache.org/tika-parser-webarchive-module/) - * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:2.9.4 - https://tika.apache.org/tika-parser-xml-module/) - * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:2.9.4 - https://tika.apache.org/tika-parser-xmp-commons/) - * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:2.9.4 - https://tika.apache.org/tika-parser-zip-commons/) - * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:2.9.4 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) + * Apache Tika core (org.apache.tika:tika-core:3.2.3 - https://tika.apache.org/) + * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:3.2.3 - https://tika.apache.org/tika-parser-apple-module/) + * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:3.2.3 - https://tika.apache.org/tika-parser-audiovideo-module/) + * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:3.2.3 - https://tika.apache.org/tika-parser-cad-module/) + * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:3.2.3 - https://tika.apache.org/tika-parser-code-module/) + * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:3.2.3 - https://tika.apache.org/tika-parser-crypto-module/) + * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:3.2.3 - https://tika.apache.org/tika-parser-digest-commons/) + * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:3.2.3 - https://tika.apache.org/tika-parser-font-module/) + * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:3.2.3 - https://tika.apache.org/tika-parser-html-module/) + * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:3.2.3 - https://tika.apache.org/tika-parser-image-module/) + * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:3.2.3 - https://tika.apache.org/tika-parser-mail-commons/) + * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:3.2.3 - https://tika.apache.org/tika-parser-mail-module/) + * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:3.2.3 - https://tika.apache.org/tika-parser-microsoft-module/) + * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:3.2.3 - https://tika.apache.org/tika-parser-miscoffice-module/) + * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:3.2.3 - https://tika.apache.org/tika-parser-news-module/) + * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:3.2.3 - https://tika.apache.org/tika-parser-ocr-module/) + * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:3.2.3 - https://tika.apache.org/tika-parser-pdf-module/) + * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:3.2.3 - https://tika.apache.org/tika-parser-pkg-module/) + * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:3.2.3 - https://tika.apache.org/tika-parser-text-module/) + * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:3.2.3 - https://tika.apache.org/tika-parser-webarchive-module/) + * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:3.2.3 - https://tika.apache.org/tika-parser-xml-module/) + * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:3.2.3 - https://tika.apache.org/tika-parser-xmp-commons/) + * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:3.2.3 - https://tika.apache.org/tika-parser-zip-commons/) + * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:3.2.3 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:9.0.83 - https://tomcat.apache.org/) * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:9.0.83 - https://tomcat.apache.org/) * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:9.0.83 - https://tomcat.apache.org/) @@ -317,43 +306,42 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * AssertJ fluent assertions (org.assertj:assertj-core:3.22.0 - https://assertj.github.io/doc/assertj-core/) * Evo Inflector (org.atteo:evo-inflector:1.3 - http://atteo.org/static/evo-inflector) * jose4j (org.bitbucket.b_c:jose4j:0.6.5 - https://bitbucket.org/b_c/jose4j/) - * TagSoup (org.ccil.cowan.tagsoup:tagsoup:1.2.1 - http://home.ccil.org/~cowan/XML/tagsoup/) * jems (org.dmfs:jems:1.18 - https://github.com/dmfs/jems) * rfc3986-uri (org.dmfs:rfc3986-uri:0.8.1 - https://github.com/dmfs/uri-toolkit) * Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.15.v20190215 - http://tomcat.apache.org/taglibs/standard/) * Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-client) * Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-client) - * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.57.v20241219 - https://jetty.org/jetty-alpn-parent/jetty-alpn-java-server/) + * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-java-server/) * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-server) - * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.57.v20241219 - https://jetty.org/jetty-alpn-parent/jetty-alpn-server/) + * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-server/) * Jetty :: Servlet Annotations (org.eclipse.jetty:jetty-annotations:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-client) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-continuation) - * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.57.v20241219 - https://jetty.org/jetty-continuation/) - * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.57.v20241219 - https://jetty.org/jetty-deploy/) - * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.57.v20241219 - https://jetty.org/jetty-http/) - * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.57.v20241219 - https://jetty.org/jetty-io/) + * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.58.v20250814 - https://jetty.org/jetty-continuation/) + * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.58.v20250814 - https://jetty.org/jetty-deploy/) + * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.58.v20250814 - https://jetty.org/jetty-http/) + * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.58.v20250814 - https://jetty.org/jetty-io/) * Jetty :: JMX Management (org.eclipse.jetty:jetty-jmx:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-jmx) * Jetty :: JNDI Naming (org.eclipse.jetty:jetty-jndi:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Plus (org.eclipse.jetty:jetty-plus:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Rewrite Handler (org.eclipse.jetty:jetty-rewrite:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-rewrite) * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-security) - * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.57.v20241219 - https://jetty.org/jetty-security/) - * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.57.v20241219 - https://jetty.org/jetty-server/) - * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.57.v20241219 - https://jetty.org/jetty-servlet/) - * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.57.v20241219 - https://jetty.org/jetty-servlets/) - * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.57.v20241219 - https://jetty.org/jetty-util/) - * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.57.v20241219 - https://jetty.org/jetty-util-ajax/) - * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.57.v20241219 - https://jetty.org/jetty-webapp/) - * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.57.v20241219 - https://jetty.org/jetty-xml/) + * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.58.v20250814 - https://jetty.org/jetty-security/) + * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.58.v20250814 - https://jetty.org/jetty-server/) + * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.58.v20250814 - https://jetty.org/jetty-servlet/) + * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.58.v20250814 - https://jetty.org/jetty-servlets/) + * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.58.v20250814 - https://jetty.org/jetty-util/) + * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.58.v20250814 - https://jetty.org/jetty-util-ajax/) + * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.58.v20250814 - https://jetty.org/jetty-webapp/) + * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.58.v20250814 - https://jetty.org/jetty-xml/) * Jetty :: HTTP2 :: Client (org.eclipse.jetty.http2:http2-client:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-client) - * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.57.v20241219 - https://jetty.org/http2-parent/http2-common/) + * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-common/) * Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-hpack) * Jetty :: HTTP2 :: HTTP Client Transport (org.eclipse.jetty.http2:http2-http-client-transport:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-http-client-transport) - * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.57.v20241219 - https://jetty.org/http2-parent/http2-server/) + * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-server/) * Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1.2 - https://eclipse.org/jetty/jetty-schemas) - * Ehcache (org.ehcache:ehcache:3.10.8 - http://ehcache.org) + * Ehcache (org.ehcache:ehcache:3.11.1 - http://ehcache.org) * flyway-core (org.flywaydb:flyway-core:8.5.13 - https://flywaydb.org/flyway-core) * Ogg and Vorbis for Java, Core (org.gagravarr:vorbis-java-core:0.8 - https://github.com/Gagravarr/VorbisJava) * Apache Tika plugin for Ogg, Vorbis and FLAC (org.gagravarr:vorbis-java-tika:0.8 - https://github.com/Gagravarr/VorbisJava) @@ -363,13 +351,11 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:6.2.5.Final - http://hibernate.org/validator/hibernate-validator) * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:6.2.5.Final - http://hibernate.org/validator/hibernate-validator-cdi) * org.immutables.value-annotations (org.immutables:value-annotations:2.9.2 - http://immutables.org/value-annotations) - * leveldb (org.iq80.leveldb:leveldb:0.12 - http://github.com/dain/leveldb/leveldb) - * leveldb-api (org.iq80.leveldb:leveldb-api:0.12 - http://github.com/dain/leveldb/leveldb-api) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) * Java Annotation Indexer (org.jboss:jandex:2.4.2.Final - http://www.jboss.org/jandex) * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.6.1.Final - http://www.jboss.org) * JDOM (org.jdom:jdom2:2.0.6.1 - http://www.jdom.org) - * IntelliJ IDEA Annotations (org.jetbrains:annotations:13.0 - http://www.jetbrains.org) + * JetBrains Java Annotations (org.jetbrains:annotations:17.0.0 - https://github.com/JetBrains/java-annotations) * Kotlin Stdlib (org.jetbrains.kotlin:kotlin-stdlib:1.8.21 - https://kotlinlang.org/) * Kotlin Stdlib Common (org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 - https://kotlinlang.org/) * Kotlin Stdlib Jdk7 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 - https://kotlinlang.org/) @@ -391,18 +377,11 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jetty Servlet Tester (org.mortbay.jetty:jetty-servlet-tester:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-servlet-tester) * Jetty Utilities (org.mortbay.jetty:jetty-util:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/jetty-util) * Servlet Specification API (org.mortbay.jetty:servlet-api:2.5-20081211 - http://jetty.mortbay.org/servlet-api) - * jwarc (org.netpreserve:jwarc:0.31.1 - https://github.com/iipc/jwarc) + * jwarc (org.netpreserve:jwarc:0.32.0 - https://github.com/iipc/jwarc) * Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis) - * org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) * parboiled-core (org.parboiled:parboiled-core:1.1.7 - http://parboiled.org) * parboiled-java (org.parboiled:parboiled-java:1.1.7 - http://parboiled.org) * RRD4J (org.rrd4j:rrd4j:3.5 - https://github.com/rrd4j/rrd4j/) - * Scala Library (org.scala-lang:scala-library:2.13.16 - https://www.scala-lang.org/) - * Scala Compiler (org.scala-lang:scala-reflect:2.13.0 - https://www.scala-lang.org/) - * scala-collection-compat (org.scala-lang.modules:scala-collection-compat_2.13:2.1.6 - http://www.scala-lang.org/) - * scala-java8-compat (org.scala-lang.modules:scala-java8-compat_2.13:0.9.0 - http://www.scala-lang.org/) - * scala-parser-combinators (org.scala-lang.modules:scala-parser-combinators_2.13:1.1.2 - http://www.scala-lang.org/) - * scala-xml (org.scala-lang.modules:scala-xml_2.13:1.3.0 - http://www.scala-lang.org/) * JSONassert (org.skyscreamer:jsonassert:1.5.1 - https://github.com/skyscreamer/JSONassert) * JCL 1.2 implemented over SLF4J (org.slf4j:jcl-over-slf4j:1.7.36 - http://www.slf4j.org) * Spring AOP (org.springframework:spring-aop:5.3.39 - https://github.com/spring-projects/spring-framework) @@ -453,10 +432,40 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * SWORD v2 :: Common Server Library (org.swordapp:sword2-server:1.0 - http://www.swordapp.org/) * snappy-java (org.xerial.snappy:snappy-java:1.1.10.1 - https://github.com/xerial/snappy-java) * xml-matchers (org.xmlmatchers:xml-matchers:0.10 - http://code.google.com/p/xml-matchers/) - * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.2 - https://www.xmlunit.org/) + * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.11.0 - https://www.xmlunit.org/) * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.9.1 - https://www.xmlunit.org/) * org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.9.1 - https://www.xmlunit.org/xmlunit-placeholders/) * SnakeYAML (org.yaml:snakeyaml:1.30 - https://bitbucket.org/snakeyaml/snakeyaml) + * AWS Java SDK :: Annotations (software.amazon.awssdk:annotations:2.38.8 - https://aws.amazon.com/sdkforjava/core/annotations) + * AWS Java SDK :: Arns (software.amazon.awssdk:arns:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Auth (software.amazon.awssdk:auth:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: AWS Core (software.amazon.awssdk:aws-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: AWS Query Protocol (software.amazon.awssdk:aws-query-protocol:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: AWS Xml Protocol (software.amazon.awssdk:aws-xml-protocol:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Checksums (software.amazon.awssdk:checksums:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Checksums SPI (software.amazon.awssdk:checksums-spi:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: AWS CRT Core (software.amazon.awssdk:crt-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Endpoints SPI (software.amazon.awssdk:endpoints-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/endpoints-spi) + * AWS Java SDK :: HTTP Auth (software.amazon.awssdk:http-auth:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth AWS (software.amazon.awssdk:http-auth-aws:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth Event Stream (software.amazon.awssdk:http-auth-aws-eventstream:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth SPI (software.amazon.awssdk:http-auth-spi:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Client Interface (software.amazon.awssdk:http-client-spi:2.38.8 - https://aws.amazon.com/sdkforjava/http-client-spi) + * AWS Java SDK :: Identity SPI (software.amazon.awssdk:identity-spi:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: Json Utils (software.amazon.awssdk:json-utils:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Metrics SPI (software.amazon.awssdk:metrics-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/metrics-spi) + * AWS Java SDK :: Profiles (software.amazon.awssdk:profiles:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: Protocol Core (software.amazon.awssdk:protocol-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Regions (software.amazon.awssdk:regions:2.38.8 - https://aws.amazon.com/sdkforjava/core/regions) + * AWS Java SDK :: Retries (software.amazon.awssdk:retries:2.38.8 - https://aws.amazon.com/sdkforjava/core/retries) + * AWS Java SDK :: Retries API (software.amazon.awssdk:retries-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/retries-spi) + * AWS Java SDK :: Services :: Amazon S3 (software.amazon.awssdk:s3:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: SDK Core (software.amazon.awssdk:sdk-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Third Party :: Jackson-core (software.amazon.awssdk:third-party-jackson-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Utilities (software.amazon.awssdk:utils:2.38.8 - https://aws.amazon.com/sdkforjava/utils) + * AWS Java SDK :: Utils Lite (software.amazon.awssdk:utils-lite:2.38.8 - https://aws.amazon.com/sdkforjava) + * software.amazon.awssdk.crt:aws-crt (software.amazon.awssdk.crt:aws-crt:0.39.4 - https://github.com/awslabs/aws-crt-java) + * AWS Event Stream (software.amazon.eventstream:eventstream:1.0.1 - https://github.com/awslabs/aws-eventstream-java) * Xerces2-j (xerces:xercesImpl:2.12.2 - https://xerces.apache.org/xerces2-j/) * XML Commons External Components XML APIs (xml-apis:xml-apis:1.4.01 - http://xml.apache.org/commons/components/external/) @@ -489,16 +498,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * asm-commons (org.ow2.asm:asm-commons:9.3 - http://asm.ow2.io/) * ASM Tree (org.ow2.asm:asm-tree:5.0.3 - http://asm.objectweb.org/asm-tree/) * ASM Util (org.ow2.asm:asm-util:5.0.3 - http://asm.objectweb.org/asm-util/) - * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.7 - https://jdbc.postgresql.org) + * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.8 - https://jdbc.postgresql.org) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) * JMatIO (org.tallison:jmatio:1.5 - https://github.com/tballison/jmatio) * XZ for Java (org.tukaani:xz:1.10 - https://tukaani.org/xz/java.html) * XMLUnit for Java (xmlunit:xmlunit:1.3 - http://xmlunit.sourceforge.net/) - CC0: - - * reactive-streams (org.reactivestreams:reactive-streams:1.0.2 - http://www.reactive-streams.org/) - Common Development and Distribution License (CDDL): * JavaMail API (com.sun.mail:javax.mail:1.6.2 - http://javaee.github.io/javamail/javax.mail) @@ -552,7 +557,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Eclipse Public License: * System Rules (com.github.stefanbirkner:system-rules:1.19.0 - http://stefanbirkner.github.io/system-rules/) - * H2 Database Engine (com.h2database:h2:2.3.232 - https://h2database.com) + * H2 Database Engine (com.h2database:h2:2.4.240 - https://h2database.com) * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:1.3.5 - https://projects.eclipse.org/projects/ee4j.ca) * javax.transaction API (jakarta.transaction:jakarta.transaction-api:1.3.3 - https://projects.eclipse.org/projects/ee4j.jta) * jakarta.ws.rs-api (jakarta.ws.rs:jakarta.ws.rs-api:2.1.6 - https://github.com/eclipse-ee4j/jaxrs-api) @@ -564,34 +569,34 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.15.v20190215 - http://tomcat.apache.org/taglibs/standard/) * Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-client) * Jetty :: ALPN :: JDK9 Client Implementation (org.eclipse.jetty:jetty-alpn-java-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-java-client) - * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.57.v20241219 - https://jetty.org/jetty-alpn-parent/jetty-alpn-java-server/) + * Jetty :: ALPN :: JDK9 Server Implementation (org.eclipse.jetty:jetty-alpn-java-server:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-java-server/) * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-server) - * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.57.v20241219 - https://jetty.org/jetty-alpn-parent/jetty-alpn-server/) + * Jetty :: ALPN :: Server (org.eclipse.jetty:jetty-alpn-server:9.4.58.v20250814 - https://jetty.org/jetty-alpn-parent/jetty-alpn-server/) * Jetty :: Servlet Annotations (org.eclipse.jetty:jetty-annotations:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-client) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-continuation) - * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.57.v20241219 - https://jetty.org/jetty-continuation/) - * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.57.v20241219 - https://jetty.org/jetty-deploy/) - * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.57.v20241219 - https://jetty.org/jetty-http/) - * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.57.v20241219 - https://jetty.org/jetty-io/) + * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.58.v20250814 - https://jetty.org/jetty-continuation/) + * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.58.v20250814 - https://jetty.org/jetty-deploy/) + * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.58.v20250814 - https://jetty.org/jetty-http/) + * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.58.v20250814 - https://jetty.org/jetty-io/) * Jetty :: JMX Management (org.eclipse.jetty:jetty-jmx:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-jmx) * Jetty :: JNDI Naming (org.eclipse.jetty:jetty-jndi:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Plus (org.eclipse.jetty:jetty-plus:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Rewrite Handler (org.eclipse.jetty:jetty-rewrite:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-rewrite) * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-security) - * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.57.v20241219 - https://jetty.org/jetty-security/) - * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.57.v20241219 - https://jetty.org/jetty-server/) - * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.57.v20241219 - https://jetty.org/jetty-servlet/) - * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.57.v20241219 - https://jetty.org/jetty-servlets/) - * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.57.v20241219 - https://jetty.org/jetty-util/) - * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.57.v20241219 - https://jetty.org/jetty-util-ajax/) - * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.57.v20241219 - https://jetty.org/jetty-webapp/) - * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.57.v20241219 - https://jetty.org/jetty-xml/) + * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.58.v20250814 - https://jetty.org/jetty-security/) + * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.58.v20250814 - https://jetty.org/jetty-server/) + * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.58.v20250814 - https://jetty.org/jetty-servlet/) + * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.58.v20250814 - https://jetty.org/jetty-servlets/) + * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.58.v20250814 - https://jetty.org/jetty-util/) + * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.58.v20250814 - https://jetty.org/jetty-util-ajax/) + * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.58.v20250814 - https://jetty.org/jetty-webapp/) + * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.58.v20250814 - https://jetty.org/jetty-xml/) * Jetty :: HTTP2 :: Client (org.eclipse.jetty.http2:http2-client:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-client) - * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.57.v20241219 - https://jetty.org/http2-parent/http2-common/) + * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-common/) * Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-hpack) * Jetty :: HTTP2 :: HTTP Client Transport (org.eclipse.jetty.http2:http2-http-client-transport:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-http-client-transport) - * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.57.v20241219 - https://jetty.org/http2-parent/http2-server/) + * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-server/) * Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1.2 - https://eclipse.org/jetty/jetty-schemas) * HK2 API module (org.glassfish.hk2:hk2-api:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:2.6.1 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) @@ -603,9 +608,6 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * jersey-core-common (org.glassfish.jersey.core:jersey-common:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * Java Persistence API, Version 2.1 (org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final - http://hibernate.org) - * JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.4 - https://junit.org/junit5/) - * JUnit Platform Engine API (org.junit.platform:junit-platform-engine:1.11.4 - https://junit.org/junit5/) - * JUnit Vintage Engine (org.junit.vintage:junit-vintage-engine:5.11.4 - https://junit.org/junit5/) * org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core) * org.locationtech.jts.io:jts-io-common (org.locationtech.jts.io:jts-io-common:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-io/jts-io-common) * Jetty Server (org.mortbay.jetty:jetty:6.1.26 - http://www.eclipse.org/jetty/jetty-parent/project/modules/jetty) @@ -652,29 +654,35 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Simple Magic (com.j256.simplemagic:simplemagic:1.17 - https://256stuff.com/sources/simplemagic/) + LGPL-2.1-or-later: + + * Java Native Access (net.java.dev.jna:jna:5.18.1 - https://github.com/java-native-access/jna) + MIT License: - * better-files (com.github.pathikrit:better-files_2.13:3.9.1 - https://github.com/pathikrit/better-files) * Java SemVer (com.github.zafarkhaja:java-semver:0.9.0 - https://github.com/zafarkhaja/jsemver) * dd-plist (com.googlecode.plist:dd-plist:1.28 - http://www.github.com/3breadt/dd-plist) * DigitalCollections: IIIF API Library (de.digitalcollections.iiif:iiif-apis:0.3.11 - https://github.com/dbmdz/iiif-apis) - * s3mock (io.findify:s3mock_2.13:0.2.6 - https://github.com/findify/s3mock) * ClassGraph (io.github.classgraph:classgraph:4.8.154 - https://github.com/classgraph/classgraph) * JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) - * Bouncy Castle JavaMail S/MIME APIs (org.bouncycastle:bcmail-jdk18on:1.80 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle JavaMail Jakarta S/MIME APIs (org.bouncycastle:bcjmail-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) * org.brotli:dec (org.brotli:dec:0.1.2 - http://brotli.org/dec) * Checker Qual (org.checkerframework:checker-qual:3.23.0 - https://checkerframework.org) - * Checker Qual (org.checkerframework:checker-qual:3.49.3 - https://checkerframework.org/) + * Checker Qual (org.checkerframework:checker-qual:3.49.5 - https://checkerframework.org/) * jersey-core-client (org.glassfish.jersey.core:jersey-client:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:2.47 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jsoup Java HTML Parser (org.jsoup:jsoup:1.21.2 - https://jsoup.org/) * mockito-core (org.mockito:mockito-core:3.12.4 - https://github.com/mockito/mockito) * mockito-inline (org.mockito:mockito-inline:3.12.4 - https://github.com/mockito/mockito) * ORCID - Model (org.orcid:orcid-model:3.0.2 - http://github.com/ORCID/orcid-model) + * Duct Tape (org.rnorth.duct-tape:duct-tape:1.0.8 - https://github.com/rnorth/duct-tape) * JUL to SLF4J bridge (org.slf4j:jul-to-slf4j:1.7.36 - http://www.slf4j.org) * SLF4J API Module (org.slf4j:slf4j-api:1.7.36 - http://www.slf4j.org) + * Testcontainers Core (org.testcontainers:testcontainers:2.0.2 - https://java.testcontainers.org) + * Testcontainers :: Localstack (org.testcontainers:testcontainers-localstack:2.0.2 - https://java.testcontainers.org) * HAL Browser (org.webjars:hal-browser:ad9b865 - http://webjars.org) * toastr (org.webjars.bowergithub.codeseven:toastr:2.1.4 - http://webjars.org) * backbone (org.webjars.bowergithub.jashkenas:backbone:1.4.1 - https://www.webjars.org) @@ -682,13 +690,17 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * jquery (org.webjars.bowergithub.jquery:jquery-dist:3.7.1 - https://www.webjars.org) * urijs (org.webjars.bowergithub.medialize:uri.js:1.19.11 - https://www.webjars.org) * bootstrap (org.webjars.bowergithub.twbs:bootstrap:4.6.2 - https://www.webjars.org) - * core-js (org.webjars.npm:core-js:3.42.0 - https://www.webjars.org) + * core-js (org.webjars.npm:core-js:3.46.0 - https://www.webjars.org) * @json-editor/json-editor (org.webjars.npm:json-editor__json-editor:2.15.2 - https://www.webjars.org) + MIT-0: + + * reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/) + Mozilla Public License: * juniversalchardet (com.github.albfernandez:juniversalchardet:2.5.0 - https://github.com/albfernandez/juniversalchardet) - * H2 Database Engine (com.h2database:h2:2.3.232 - https://h2database.com) + * H2 Database Engine (com.h2database:h2:2.4.240 - https://h2database.com) * Saxon-HE (net.sf.saxon:Saxon-HE:9.9.1-8 - http://www.saxonica.com/) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) * Mozilla Rhino (org.mozilla:rhino:1.7.7.2 - https://developer.mozilla.org/en/Rhino) @@ -703,6 +715,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) + The Apache Software License, version 2.0: + + * picocli (info.picocli:picocli:4.7.6 - https://picocli.info) + UnRar License: * Java Unrar (com.github.junrar:junrar:7.5.5 - https://github.com/junrar/junrar) From bcf38e11840efc7835e4881bc260983aa7555a20 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 16 Dec 2025 15:51:03 -0600 Subject: [PATCH 479/701] Update LICENSES_THIRD_PARTY for 8.3 release --- LICENSES_THIRD_PARTY | 456 ++++++++++++++++++++++--------------------- 1 file changed, 237 insertions(+), 219 deletions(-) diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index 5d99bd7e426c..d6b30f3fd1f8 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -21,35 +21,34 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Apache Software License, Version 2.0: * Ant-Contrib Tasks (ant-contrib:ant-contrib:1.0b3 - http://ant-contrib.sourceforge.net) - * AWS SDK for Java - Core (com.amazonaws:aws-java-sdk-core:1.12.785 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK for AWS KMS (com.amazonaws:aws-java-sdk-kms:1.12.785 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK for Amazon S3 (com.amazonaws:aws-java-sdk-s3:1.12.785 - https://aws.amazon.com/sdkforjava) - * JMES Path Query library (com.amazonaws:jmespath-java:1.12.785 - https://aws.amazon.com/sdkforjava) + * S3Mock - Testsupport - Testcontainers (com.adobe.testing:s3mock-testcontainers:4.10.0 - https://www.github.com/adobe/S3Mock/s3mock-testsupport-reactor/s3mock-testcontainers) * Titanium JSON-LD 1.1 (JRE11) (com.apicatalog:titanium-json-ld:1.3.2 - https://github.com/filip26/titanium-json-ld) * HPPC Collections (com.carrotsearch:hppc:0.8.1 - http://labs.carrotsearch.com/hppc.html/hppc) * com.drewnoakes:metadata-extractor (com.drewnoakes:metadata-extractor:2.19.0 - https://drewnoakes.com/code/exif/) * parso (com.epam:parso:2.0.14 - https://github.com/epam/parso) * Internet Time Utility (com.ethlo.time:itu:1.7.0 - https://github.com/ethlo/itu) - * ClassMate (com.fasterxml:classmate:1.7.0 - https://github.com/FasterXML/java-classmate) - * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.19.1 - https://github.com/FasterXML/jackson) - * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.19.1 - https://github.com/FasterXML/jackson-core) - * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.19.1 - https://github.com/FasterXML/jackson) - * Jackson dataformat: CBOR (com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.17.2 - https://github.com/FasterXML/jackson-dataformats-binary) + * ClassMate (com.fasterxml:classmate:1.7.1 - https://github.com/FasterXML/java-classmate) + * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.20 - https://github.com/FasterXML/jackson) + * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.20.1 - https://github.com/FasterXML/jackson-core) + * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.20.1 - https://github.com/FasterXML/jackson) * Jackson dataformat: Smile (com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.15.2 - https://github.com/FasterXML/jackson-dataformats-binary) * Jackson-dataformat-TOML (com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.15.2 - https://github.com/FasterXML/jackson-dataformats-text) * Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.2 - https://github.com/FasterXML/jackson-dataformats-text) - * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) - * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) + * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.19.4 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) + * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) * Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.16.2 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) * Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.16.2 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) * Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.16.2 - https://github.com/FasterXML/jackson-modules-base) - * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.19.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) + * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.19.4 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) * Java UUID Generator (com.fasterxml.uuid:java-uuid-generator:4.1.0 - https://github.com/cowtowncoder/java-uuid-generator) * Woodstox (com.fasterxml.woodstox:woodstox-core:6.5.1 - https://github.com/FasterXML/woodstox) * zjsonpatch (com.flipkart.zjsonpatch:zjsonpatch:0.4.16 - https://github.com/flipkart-incubator/zjsonpatch/) * Caffeine cache (com.github.ben-manes.caffeine:caffeine:2.9.3 - https://github.com/ben-manes/caffeine) * Caffeine cache (com.github.ben-manes.caffeine:caffeine:3.1.8 - https://github.com/ben-manes/caffeine) * JSON.simple (com.github.cliftonlabs:json-simple:3.0.2 - https://cliftonlabs.github.io/json-simple/) + * docker-java-api (com.github.docker-java:docker-java-api:3.4.2 - https://github.com/docker-java/docker-java) + * docker-java-transport (com.github.docker-java:docker-java-transport:3.4.2 - https://github.com/docker-java/docker-java) + * docker-java-transport-zerodep (com.github.docker-java:docker-java-transport-zerodep:3.4.2 - https://github.com/docker-java/docker-java) * btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) * jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) * jackson-coreutils-equivalence (com.github.java-json-tools:jackson-coreutils-equivalence:1.0 - https://github.com/java-json-tools/jackson-coreutils) @@ -60,25 +59,25 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * uri-template (com.github.java-json-tools:uri-template:0.10 - https://github.com/java-json-tools/uri-template) * JCIP Annotations under Apache License (com.github.stephenc.jcip:jcip-annotations:1.0-1 - http://stephenc.github.com/jcip-annotations) * FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/) - * Gson (com.google.code.gson:gson:2.13.1 - https://github.com/google/gson) - * error-prone annotations (com.google.errorprone:error_prone_annotations:2.38.0 - https://errorprone.info/error_prone_annotations) + * Gson (com.google.code.gson:gson:2.13.2 - https://github.com/google/gson) + * error-prone annotations (com.google.errorprone:error_prone_annotations:2.42.0 - https://errorprone.info/error_prone_annotations) * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess) * Guava: Google Core Libraries for Java (com.google.guava:guava:32.1.3-jre - https://github.com/google/guava) * Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:2.8 - https://github.com/google/j2objc/) * libphonenumber (com.googlecode.libphonenumber:libphonenumber:8.11.1 - https://github.com/google/libphonenumber/) - * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.8 - https://jackcess.sourceforge.io) + * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.10 - https://jackcess.sourceforge.io) * Jackcess Encrypt (com.healthmarketscience.jackcess:jackcess-encrypt:4.0.3 - http://jackcessencrypt.sf.net) - * json-path (com.jayway.jsonpath:json-path:2.9.0 - https://github.com/jayway/JsonPath) - * json-path-assert (com.jayway.jsonpath:json-path-assert:2.9.0 - https://github.com/jayway/JsonPath) + * json-path (com.jayway.jsonpath:json-path:2.10.0 - https://github.com/jayway/JsonPath) + * json-path-assert (com.jayway.jsonpath:json-path-assert:2.10.0 - https://github.com/jayway/JsonPath) * Disruptor Framework (com.lmax:disruptor:3.4.2 - http://lmax-exchange.github.com/disruptor) * MaxMind DB Reader (com.maxmind.db:maxmind-db:2.1.0 - http://dev.maxmind.com/) * MaxMind GeoIP2 API (com.maxmind.geoip2:geoip2:2.17.0 - https://dev.maxmind.com/geoip?lang=en) * JsonSchemaValidator (com.networknt:json-schema-validator:1.0.76 - https://github.com/networknt/json-schema-validator) * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.28 - https://bitbucket.org/connect2id/nimbus-jose-jwt) * Nimbus JOSE+JWT (com.nimbusds:nimbus-jose-jwt:9.48 - https://bitbucket.org/connect2id/nimbus-jose-jwt) - * opencsv (com.opencsv:opencsv:5.11.1 - http://opencsv.sf.net) + * opencsv (com.opencsv:opencsv:5.12.0 - http://opencsv.sf.net) * java-libpst (com.pff:java-libpst:0.9.3 - https://github.com/rjohnsondev/java-libpst) * rome (com.rometools:rome:1.19.0 - http://rometools.com/rome) * rome-modules (com.rometools:rome-modules:1.19.0 - http://rometools.com/rome-modules) @@ -88,26 +87,17 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * okio (com.squareup.okio:okio:3.6.0 - https://github.com/square/okio/) * okio (com.squareup.okio:okio-jvm:3.6.0 - https://github.com/square/okio/) * T-Digest (com.tdunning:t-digest:3.1 - https://github.com/tdunning/t-digest) - * config (com.typesafe:config:1.3.3 - https://github.com/lightbend/config) - * ssl-config-core (com.typesafe:ssl-config-core_2.13:0.3.8 - https://github.com/lightbend/ssl-config) - * akka-actor (com.typesafe.akka:akka-actor_2.13:2.5.31 - https://akka.io/) - * akka-http-core (com.typesafe.akka:akka-http-core_2.13:10.1.12 - https://akka.io) - * akka-http (com.typesafe.akka:akka-http_2.13:10.1.12 - https://akka.io) - * akka-parsing (com.typesafe.akka:akka-parsing_2.13:10.1.12 - https://akka.io) - * akka-protobuf (com.typesafe.akka:akka-protobuf_2.13:2.5.31 - https://akka.io/) - * akka-stream (com.typesafe.akka:akka-stream_2.13:2.5.31 - https://akka.io/) - * scala-logging (com.typesafe.scala-logging:scala-logging_2.13:3.9.2 - https://github.com/lightbend/scala-logging) * JSON library from Android SDK (com.vaadin.external.google:android-json:0.0.20131108.vaadin1 - http://developer.android.com/sdk) * SparseBitSet (com.zaxxer:SparseBitSet:1.3 - https://github.com/brettwooldridge/SparseBitSet) * Apache Commons BeanUtils (commons-beanutils:commons-beanutils:1.11.0 - https://commons.apache.org/proper/commons-beanutils) - * Apache Commons CLI (commons-cli:commons-cli:1.9.0 - https://commons.apache.org/proper/commons-cli/) - * Apache Commons Codec (commons-codec:commons-codec:1.18.0 - https://commons.apache.org/proper/commons-codec/) + * Apache Commons CLI (commons-cli:commons-cli:1.11.0 - https://commons.apache.org/proper/commons-cli/) + * Apache Commons Codec (commons-codec:commons-codec:1.20.0 - https://commons.apache.org/proper/commons-codec/) * Apache Commons Collections (commons-collections:commons-collections:3.2.2 - http://commons.apache.org/collections/) * Commons Digester (commons-digester:commons-digester:2.1 - http://commons.apache.org/digester/) - * Apache Commons IO (commons-io:commons-io:2.19.0 - https://commons.apache.org/proper/commons-io/) + * Apache Commons IO (commons-io:commons-io:2.21.0 - https://commons.apache.org/proper/commons-io/) * Commons Lang (commons-lang:commons-lang:2.6 - http://commons.apache.org/lang/) * Apache Commons Logging (commons-logging:commons-logging:1.3.5 - https://commons.apache.org/proper/commons-logging/) - * Apache Commons Validator (commons-validator:commons-validator:1.9.0 - http://commons.apache.org/proper/commons-validator/) + * Apache Commons Validator (commons-validator:commons-validator:1.10.1 - https://commons.apache.org/proper/commons-validator/) * GeoJson POJOs for Jackson (de.grundid.opendatalab:geojson-jackson:1.14 - https://github.com/opendatalab-de/geojson-jackson) * broker-client (eu.openaire:broker-client:1.1.2 - http://api.openaire.eu/broker/broker-client) * OpenAIRE Funders Model (eu.openaire:funders-model:2.0.0 - https://api.openaire.eu) @@ -117,10 +107,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Metrics Integration with JMX (io.dropwizard.metrics:metrics-jmx:4.1.5 - https://metrics.dropwizard.io/metrics-jmx) * JVM Integration for Metrics (io.dropwizard.metrics:metrics-jvm:4.1.5 - https://metrics.dropwizard.io/metrics-jvm) * SWORD v2 Common Server Library (forked) (io.gdcc:sword2-server:2.0.0 - https://github.com/gdcc/sword2-server) - * micrometer-commons (io.micrometer:micrometer-commons:1.14.8 - https://github.com/micrometer-metrics/micrometer) - * micrometer-core (io.micrometer:micrometer-core:1.15.1 - https://github.com/micrometer-metrics/micrometer) - * micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.15.1 - https://github.com/micrometer-metrics/micrometer) - * micrometer-observation (io.micrometer:micrometer-observation:1.14.8 - https://github.com/micrometer-metrics/micrometer) + * micrometer-commons (io.micrometer:micrometer-commons:1.14.13 - https://github.com/micrometer-metrics/micrometer) + * micrometer-core (io.micrometer:micrometer-core:1.15.6 - https://github.com/micrometer-metrics/micrometer) + * micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.15.6 - https://github.com/micrometer-metrics/micrometer) + * micrometer-observation (io.micrometer:micrometer-observation:1.14.13 - https://github.com/micrometer-metrics/micrometer) * Netty/Buffer (io.netty:netty-buffer:4.1.99.Final - https://netty.io/netty-buffer/) * Netty/Codec (io.netty:netty-codec:4.1.99.Final - https://netty.io/netty-codec/) * Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.86.Final - https://netty.io/netty-codec-http/) @@ -168,39 +158,38 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jakarta Bean Validation API (jakarta.validation:jakarta.validation-api:3.0.2 - https://beanvalidation.org) * JSR107 API and SPI (javax.cache:cache-api:1.1.1 - https://github.com/jsr107/jsr107spec) * jdbm (jdbm:jdbm:1.0 - no url defined) - * Joda-Time (joda-time:joda-time:2.12.7 - https://www.joda.org/joda-time/) + * Joda-Time (joda-time:joda-time:2.10.5 - https://www.joda.org/joda-time/) * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.11.13 - https://bytebuddy.net/byte-buddy) * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.14.11 - https://bytebuddy.net/byte-buddy) * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.11.13 - https://bytebuddy.net/byte-buddy-agent) * eigenbase-properties (net.hydromatic:eigenbase-properties:1.1.5 - http://github.com/julianhyde/eigenbase-properties) + * Java Native Access (net.java.dev.jna:jna:5.13.0 - https://github.com/java-native-access/jna) * json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.36.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) * "Java Concurrency in Practice" book annotations (net.jcip:jcip-annotations:1.0 - http://jcip.net/) - * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.0 - https://urielch.github.io/) - * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.5.2 - https://urielch.github.io/) - * JSON Small and Fast Parser (net.minidev:json-smart:2.5.0 - https://urielch.github.io/) - * JSON Small and Fast Parser (net.minidev:json-smart:2.5.2 - https://urielch.github.io/) + * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.6.0 - https://urielch.github.io/) + * JSON Small and Fast Parser (net.minidev:json-smart:2.6.0 - https://urielch.github.io/) * Abdera Core (org.apache.abdera:abdera-core:1.1.3 - http://abdera.apache.org/abdera-core) * I18N Libraries (org.apache.abdera:abdera-i18n:1.1.3 - http://abdera.apache.org) * Abdera Parser (org.apache.abdera:abdera-parser:1.1.3 - http://abdera.apache.org/abdera-parser) * Apache Ant Core (org.apache.ant:ant:1.10.15 - https://ant.apache.org/) * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.15 - https://ant.apache.org/) - * Apache Commons BCEL (org.apache.bcel:bcel:6.10.0 - https://commons.apache.org/proper/commons-bcel) + * Apache Commons BCEL (org.apache.bcel:bcel:6.11.0 - https://commons.apache.org/proper/commons-bcel) * Calcite Core (org.apache.calcite:calcite-core:1.35.0 - https://calcite.apache.org) * Calcite Linq4j (org.apache.calcite:calcite-linq4j:1.35.0 - https://calcite.apache.org) * Apache Calcite Avatica (org.apache.calcite.avatica:avatica-core:1.23.0 - https://calcite.apache.org/avatica) * Apache Calcite Avatica Metrics (org.apache.calcite.avatica:avatica-metrics:1.23.0 - https://calcite.apache.org/avatica) * Apache Commons Collections (org.apache.commons:commons-collections4:4.5.0 - https://commons.apache.org/proper/commons-collections/) - * Apache Commons Compress (org.apache.commons:commons-compress:1.27.1 - https://commons.apache.org/proper/commons-compress/) - * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.12.0 - https://commons.apache.org/proper/commons-configuration/) - * Apache Commons CSV (org.apache.commons:commons-csv:1.14.0 - https://commons.apache.org/proper/commons-csv/) + * Apache Commons Compress (org.apache.commons:commons-compress:1.28.0 - https://commons.apache.org/proper/commons-compress/) + * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.13.0 - https://commons.apache.org/proper/commons-configuration/) + * Apache Commons CSV (org.apache.commons:commons-csv:1.14.1 - https://commons.apache.org/proper/commons-csv/) * Apache Commons DBCP (org.apache.commons:commons-dbcp2:2.13.0 - https://commons.apache.org/proper/commons-dbcp/) * Apache Commons Digester (org.apache.commons:commons-digester3:3.2 - http://commons.apache.org/digester/) * Apache Commons Exec (org.apache.commons:commons-exec:1.3 - http://commons.apache.org/proper/commons-exec/) - * Apache Commons Exec (org.apache.commons:commons-exec:1.4.0 - https://commons.apache.org/proper/commons-exec/) - * Apache Commons Lang (org.apache.commons:commons-lang3:3.17.0 - https://commons.apache.org/proper/commons-lang/) + * Apache Commons Exec (org.apache.commons:commons-exec:1.5.0 - https://commons.apache.org/proper/commons-exec/) + * Apache Commons Lang (org.apache.commons:commons-lang3:3.20.0 - https://commons.apache.org/proper/commons-lang/) * Apache Commons Math (org.apache.commons:commons-math3:3.6.1 - http://commons.apache.org/proper/commons-math/) * Apache Commons Pool (org.apache.commons:commons-pool2:2.12.1 - https://commons.apache.org/proper/commons-pool/) - * Apache Commons Text (org.apache.commons:commons-text:1.13.1 - https://commons.apache.org/proper/commons-text) + * Apache Commons Text (org.apache.commons:commons-text:1.14.0 - https://commons.apache.org/proper/commons-text) * Curator Client (org.apache.curator:curator-client:2.13.0 - http://curator.apache.org/curator-client) * Curator Framework (org.apache.curator:curator-framework:2.13.0 - http://curator.apache.org/curator-framework) * Curator Recipes (org.apache.curator:curator-recipes:2.13.0 - http://curator.apache.org/curator-recipes) @@ -214,13 +203,13 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga) * Apache HttpClient Mime (org.apache.httpcomponents:httpmime:4.5.14 - http://hc.apache.org/httpcomponents-client-ga) * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.1.3 - https://hc.apache.org/httpcomponents-client-5.0.x/5.1.3/httpclient5/) - * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.5 - https://hc.apache.org/httpcomponents-client-5.5.x/5.5/httpclient5/) + * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.5.1 - https://hc.apache.org/httpcomponents-client-5.5.x/5.5.1/httpclient5/) * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5/) - * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.3.4 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.4/httpcore5/) + * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.3.6 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.6/httpcore5/) * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5-h2/) - * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.3.4 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.4/httpcore5-h2/) - * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.12 - http://james.apache.org/mime4j/apache-mime4j-core) - * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.12 - http://james.apache.org/mime4j/apache-mime4j-dom) + * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.3.6 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.6/httpcore5-h2/) + * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.13 - http://james.apache.org/mime4j/apache-mime4j-core) + * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.13 - http://james.apache.org/mime4j/apache-mime4j-dom) * Apache Jena - Libraries POM (org.apache.jena:apache-jena-libs:4.10.0 - https://jena.apache.org/apache-jena-libs/) * Apache Jena - ARQ (org.apache.jena:jena-arq:4.10.0 - https://jena.apache.org/jena-arq/) * Apache Jena - Base (org.apache.jena:jena-base:4.10.0 - https://jena.apache.org/jena-base/) @@ -242,12 +231,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Kerby ASN1 Project (org.apache.kerby:kerby-asn1:1.0.1 - http://directory.apache.org/kerby/kerby-common/kerby-asn1) * Kerby PKIX Project (org.apache.kerby:kerby-pkix:1.0.1 - http://directory.apache.org/kerby/kerby-pkix) * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-1.2-api/) - * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-api/) - * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-core/) + * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.2 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.2 - https://logging.apache.org/log4j/2.x/) * Apache Log4j JUL Adapter (org.apache.logging.log4j:log4j-jul:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-jul/) * Apache Log4j Layout for JSON template (org.apache.logging.log4j:log4j-layout-template-json:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-layout-template-json/) * Apache Log4j SLF4J Binding (org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/) - * SLF4J 2 Provider for Log4j API (org.apache.logging.log4j:log4j-slf4j2-impl:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-slf4j2-impl/) + * SLF4J 2 Provider for Log4j API (org.apache.logging.log4j:log4j-slf4j2-impl:2.25.2 - https://logging.apache.org/log4j/2.x/) * Apache Log4j Web (org.apache.logging.log4j:log4j-web:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-web/) * Lucene Common Analyzers (org.apache.lucene:lucene-analyzers-common:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-common) * Lucene ICU Analysis Components (org.apache.lucene:lucene-analyzers-icu:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-icu) @@ -272,12 +261,13 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Lucene Spatial Extras (org.apache.lucene:lucene-spatial-extras:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial-extras) * Lucene Spatial 3D (org.apache.lucene:lucene-spatial3d:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial3d) * Lucene Suggest (org.apache.lucene:lucene-suggest:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-suggest) - * Apache FontBox (org.apache.pdfbox:fontbox:2.0.34 - http://pdfbox.apache.org/) + * Apache FontBox (org.apache.pdfbox:fontbox:3.0.5 - http://pdfbox.apache.org/) * PDFBox JBIG2 ImageIO plugin (org.apache.pdfbox:jbig2-imageio:3.0.4 - https://www.apache.org/jbig2-imageio/) * Apache JempBox (org.apache.pdfbox:jempbox:1.8.17 - http://www.apache.org/pdfbox-parent/jempbox/) - * Apache PDFBox (org.apache.pdfbox:pdfbox:2.0.34 - https://www.apache.org/pdfbox-parent/pdfbox/) - * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:2.0.34 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) - * Apache XmpBox (org.apache.pdfbox:xmpbox:2.0.34 - https://www.apache.org/pdfbox-parent/xmpbox/) + * Apache PDFBox (org.apache.pdfbox:pdfbox:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox/) + * Apache PDFBox io (org.apache.pdfbox:pdfbox-io:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox-io/) + * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) + * Apache XmpBox (org.apache.pdfbox:xmpbox:3.0.5 - https://www.apache.org/pdfbox-parent/xmpbox/) * Apache POI - Common (org.apache.poi:poi:5.4.1 - https://poi.apache.org/) * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.4.1 - https://poi.apache.org/) * Apache POI (org.apache.poi:poi-ooxml-lite:5.4.1 - https://poi.apache.org/) @@ -287,33 +277,33 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache Standard Taglib Implementation (org.apache.taglibs:taglibs-standard-impl:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-impl) * Apache Standard Taglib Specification API (org.apache.taglibs:taglibs-standard-spec:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-spec) * Apache Thrift (org.apache.thrift:libthrift:0.19.0 - http://thrift.apache.org) - * Apache Tika core (org.apache.tika:tika-core:2.9.4 - https://tika.apache.org/) - * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:2.9.4 - https://tika.apache.org/tika-parser-apple-module/) - * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:2.9.4 - https://tika.apache.org/tika-parser-audiovideo-module/) - * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:2.9.4 - https://tika.apache.org/tika-parser-cad-module/) - * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:2.9.4 - https://tika.apache.org/tika-parser-code-module/) - * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:2.9.4 - https://tika.apache.org/tika-parser-crypto-module/) - * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:2.9.4 - https://tika.apache.org/tika-parser-digest-commons/) - * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:2.9.4 - https://tika.apache.org/tika-parser-font-module/) - * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:2.9.4 - https://tika.apache.org/tika-parser-html-module/) - * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:2.9.4 - https://tika.apache.org/tika-parser-image-module/) - * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:2.9.4 - https://tika.apache.org/tika-parser-mail-commons/) - * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:2.9.4 - https://tika.apache.org/tika-parser-mail-module/) - * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:2.9.4 - https://tika.apache.org/tika-parser-microsoft-module/) - * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:2.9.4 - https://tika.apache.org/tika-parser-miscoffice-module/) - * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:2.9.4 - https://tika.apache.org/tika-parser-news-module/) - * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:2.9.4 - https://tika.apache.org/tika-parser-ocr-module/) - * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:2.9.4 - https://tika.apache.org/tika-parser-pdf-module/) - * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:2.9.4 - https://tika.apache.org/tika-parser-pkg-module/) - * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:2.9.4 - https://tika.apache.org/tika-parser-text-module/) - * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:2.9.4 - https://tika.apache.org/tika-parser-webarchive-module/) - * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:2.9.4 - https://tika.apache.org/tika-parser-xml-module/) - * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:2.9.4 - https://tika.apache.org/tika-parser-xmp-commons/) - * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:2.9.4 - https://tika.apache.org/tika-parser-zip-commons/) - * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:2.9.4 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) - * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:10.1.42 - https://tomcat.apache.org/) - * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:10.1.42 - https://tomcat.apache.org/) - * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:10.1.42 - https://tomcat.apache.org/) + * Apache Tika core (org.apache.tika:tika-core:3.2.3 - https://tika.apache.org/) + * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:3.2.3 - https://tika.apache.org/tika-parser-apple-module/) + * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:3.2.3 - https://tika.apache.org/tika-parser-audiovideo-module/) + * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:3.2.3 - https://tika.apache.org/tika-parser-cad-module/) + * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:3.2.3 - https://tika.apache.org/tika-parser-code-module/) + * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:3.2.3 - https://tika.apache.org/tika-parser-crypto-module/) + * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:3.2.3 - https://tika.apache.org/tika-parser-digest-commons/) + * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:3.2.3 - https://tika.apache.org/tika-parser-font-module/) + * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:3.2.3 - https://tika.apache.org/tika-parser-html-module/) + * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:3.2.3 - https://tika.apache.org/tika-parser-image-module/) + * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:3.2.3 - https://tika.apache.org/tika-parser-mail-commons/) + * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:3.2.3 - https://tika.apache.org/tika-parser-mail-module/) + * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:3.2.3 - https://tika.apache.org/tika-parser-microsoft-module/) + * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:3.2.3 - https://tika.apache.org/tika-parser-miscoffice-module/) + * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:3.2.3 - https://tika.apache.org/tika-parser-news-module/) + * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:3.2.3 - https://tika.apache.org/tika-parser-ocr-module/) + * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:3.2.3 - https://tika.apache.org/tika-parser-pdf-module/) + * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:3.2.3 - https://tika.apache.org/tika-parser-pkg-module/) + * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:3.2.3 - https://tika.apache.org/tika-parser-text-module/) + * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:3.2.3 - https://tika.apache.org/tika-parser-webarchive-module/) + * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:3.2.3 - https://tika.apache.org/tika-parser-xml-module/) + * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:3.2.3 - https://tika.apache.org/tika-parser-xmp-commons/) + * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:3.2.3 - https://tika.apache.org/tika-parser-zip-commons/) + * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:3.2.3 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) + * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:10.1.49 - https://tomcat.apache.org/) + * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:10.1.49 - https://tomcat.apache.org/) + * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:10.1.49 - https://tomcat.apache.org/) * Apache Velocity - Engine (org.apache.velocity:velocity-engine-core:2.4.1 - http://velocity.apache.org/engine/devel/velocity-engine-core/) * Apache Velocity - JSR 223 Scripting (org.apache.velocity:velocity-engine-scripting:2.3 - http://velocity.apache.org/engine/devel/velocity-engine-scripting/) * Apache Velocity Tools - Generic tools (org.apache.velocity.tools:velocity-tools-generic:3.1 - https://velocity.apache.org/tools/devel/velocity-tools-generic/) @@ -323,12 +313,11 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache ZooKeeper - Server (org.apache.zookeeper:zookeeper:3.6.2 - http://zookeeper.apache.org/zookeeper) * Apache ZooKeeper - Jute (org.apache.zookeeper:zookeeper-jute:3.6.2 - http://zookeeper.apache.org/zookeeper-jute) * org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) - * AssertJ Core (org.assertj:assertj-core:3.27.3 - https://assertj.github.io/doc/#assertj-core) + * AssertJ Core (org.assertj:assertj-core:3.27.6 - https://assertj.github.io/doc/#assertj-core) * Evo Inflector (org.atteo:evo-inflector:1.3 - http://atteo.org/static/evo-inflector) * attoparser (org.attoparser:attoparser:2.0.7.RELEASE - https://www.attoparser.org) * Awaitility (org.awaitility:awaitility:4.2.2 - http://awaitility.org) * jose4j (org.bitbucket.b_c:jose4j:0.6.5 - https://bitbucket.org/b_c/jose4j/) - * TagSoup (org.ccil.cowan.tagsoup:tagsoup:1.2.1 - http://home.ccil.org/~cowan/XML/tagsoup/) * Woodstox (org.codehaus.woodstox:wstx-asl:3.2.6 - http://woodstox.codehaus.org) * jems (org.dmfs:jems:1.18 - https://github.com/dmfs/jems) * rfc3986-uri (org.dmfs:rfc3986-uri:0.8.1 - https://github.com/dmfs/uri-toolkit) @@ -344,122 +333,145 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-client) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-continuation) - * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.57.v20241219 - https://jetty.org/jetty-deploy/) - * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.57.v20241219 - https://jetty.org/jetty-http/) - * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.57.v20241219 - https://jetty.org/jetty-io/) + * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.58.v20250814 - https://jetty.org/jetty-deploy/) + * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.58.v20250814 - https://jetty.org/jetty-http/) + * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.58.v20250814 - https://jetty.org/jetty-io/) * Jetty :: JMX Management (org.eclipse.jetty:jetty-jmx:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-jmx) * Jetty :: JNDI Naming (org.eclipse.jetty:jetty-jndi:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Plus (org.eclipse.jetty:jetty-plus:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Rewrite Handler (org.eclipse.jetty:jetty-rewrite:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-rewrite) * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-security) - * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.57.v20241219 - https://jetty.org/jetty-security/) - * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.57.v20241219 - https://jetty.org/jetty-server/) - * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.57.v20241219 - https://jetty.org/jetty-servlet/) + * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.58.v20250814 - https://jetty.org/jetty-security/) + * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.58.v20250814 - https://jetty.org/jetty-server/) + * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.58.v20250814 - https://jetty.org/jetty-servlet/) * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-servlets) - * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.57.v20241219 - https://jetty.org/jetty-util/) - * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.57.v20241219 - https://jetty.org/jetty-util-ajax/) - * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.57.v20241219 - https://jetty.org/jetty-webapp/) + * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.58.v20250814 - https://jetty.org/jetty-util/) + * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.58.v20250814 - https://jetty.org/jetty-util-ajax/) + * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.58.v20250814 - https://jetty.org/jetty-webapp/) * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-xml) - * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.57.v20241219 - https://jetty.org/jetty-xml/) + * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.58.v20250814 - https://jetty.org/jetty-xml/) * Jetty :: ALPN :: API (org.eclipse.jetty.alpn:alpn-api:1.1.3.v20160715 - http://www.eclipse.org/jetty/alpn-api) * Jetty :: HTTP2 :: Client (org.eclipse.jetty.http2:http2-client:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-client) - * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.57.v20241219 - https://jetty.org/http2-parent/http2-common/) + * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-common/) * Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-hpack) * Jetty :: HTTP2 :: HTTP Client Transport (org.eclipse.jetty.http2:http2-http-client-transport:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-http-client-transport) * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.15.v20190215 - https://eclipse.org/jetty/http2-parent/http2-server) * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-server) * Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1.2 - https://eclipse.org/jetty/jetty-schemas) - * Ehcache (org.ehcache:ehcache:3.10.8 - http://ehcache.org) + * Ehcache (org.ehcache:ehcache:3.11.1 - http://ehcache.org) * flyway-core (org.flywaydb:flyway-core:10.22.0 - https://flywaydb.org/flyway-core) * flyway-database-postgresql (org.flywaydb:flyway-database-postgresql:10.22.0 - https://flywaydb.org/flyway-database-postgresql) * Ogg and Vorbis for Java, Core (org.gagravarr:vorbis-java-core:0.8 - https://github.com/Gagravarr/VorbisJava) * Apache Tika plugin for Ogg, Vorbis and FLAC (org.gagravarr:vorbis-java-tika:0.8 - https://github.com/Gagravarr/VorbisJava) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) - * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:8.0.2.Final - http://hibernate.org/validator/hibernate-validator) - * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:8.0.2.Final - http://hibernate.org/validator/hibernate-validator-cdi) + * Hibernate Validator Engine (org.hibernate.validator:hibernate-validator:8.0.3.Final - https://hibernate.org/validator) + * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:8.0.3.Final - https://hibernate.org/validator) * org.immutables.value-annotations (org.immutables:value-annotations:2.9.2 - http://immutables.org/value-annotations) - * leveldb (org.iq80.leveldb:leveldb:0.12 - http://github.com/dain/leveldb/leveldb) - * leveldb-api (org.iq80.leveldb:leveldb-api:0.12 - http://github.com/dain/leveldb/leveldb-api) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.6.1.Final - http://www.jboss.org) * JDOM (org.jdom:jdom2:2.0.6.1 - http://www.jdom.org) - * IntelliJ IDEA Annotations (org.jetbrains:annotations:13.0 - http://www.jetbrains.org) + * JetBrains Java Annotations (org.jetbrains:annotations:17.0.0 - https://github.com/JetBrains/java-annotations) * Kotlin Stdlib (org.jetbrains.kotlin:kotlin-stdlib:1.8.21 - https://kotlinlang.org/) * Kotlin Stdlib Common (org.jetbrains.kotlin:kotlin-stdlib-common:1.8.21 - https://kotlinlang.org/) * Kotlin Stdlib Jdk7 (org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.21 - https://kotlinlang.org/) * Kotlin Stdlib Jdk8 (org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.21 - https://kotlinlang.org/) + * JSpecify annotations (org.jspecify:jspecify:1.0.0 - http://jspecify.org/) * Proj4J (org.locationtech.proj4j:proj4j:1.1.5 - https://github.com/locationtech/proj4j) * Spatial4J (org.locationtech.spatial4j:spatial4j:0.7 - https://projects.eclipse.org/projects/locationtech.spatial4j) * MockServer Java Client (org.mock-server:mockserver-client-java:5.15.0 - https://www.mock-server.com) * MockServer Core (org.mock-server:mockserver-core:5.15.0 - https://www.mock-server.com) * MockServer JUnit 4 Integration (org.mock-server:mockserver-junit-rule:5.15.0 - https://www.mock-server.com) * MockServer & Proxy Netty (org.mock-server:mockserver-netty:5.15.0 - https://www.mock-server.com) - * jwarc (org.netpreserve:jwarc:0.31.1 - https://github.com/iipc/jwarc) + * jwarc (org.netpreserve:jwarc:0.32.0 - https://github.com/iipc/jwarc) * Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis) - * org.opentest4j:opentest4j (org.opentest4j:opentest4j:1.3.0 - https://github.com/ota4j-team/opentest4j) * org.roaringbitmap:RoaringBitmap (org.roaringbitmap:RoaringBitmap:1.0.0 - https://github.com/RoaringBitmap/RoaringBitmap) * RRD4J (org.rrd4j:rrd4j:3.5 - https://github.com/rrd4j/rrd4j/) - * Scala Library (org.scala-lang:scala-library:2.13.2 - https://www.scala-lang.org/) - * Scala Compiler (org.scala-lang:scala-reflect:2.13.0 - https://www.scala-lang.org/) - * scala-collection-compat (org.scala-lang.modules:scala-collection-compat_2.13:2.1.6 - http://www.scala-lang.org/) - * scala-java8-compat (org.scala-lang.modules:scala-java8-compat_2.13:0.9.0 - http://www.scala-lang.org/) - * scala-parser-combinators (org.scala-lang.modules:scala-parser-combinators_2.13:1.1.2 - http://www.scala-lang.org/) - * scala-xml (org.scala-lang.modules:scala-xml_2.13:1.3.0 - http://www.scala-lang.org/) * JSONassert (org.skyscreamer:jsonassert:1.5.3 - https://github.com/skyscreamer/JSONassert) * JCL 1.2 implemented over SLF4J (org.slf4j:jcl-over-slf4j:2.0.17 - http://www.slf4j.org) - * Spring AOP (org.springframework:spring-aop:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Beans (org.springframework:spring-beans:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Context (org.springframework:spring-context:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Context Support (org.springframework:spring-context-support:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Core (org.springframework:spring-core:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Commons Logging Bridge (org.springframework:spring-jcl:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring JDBC (org.springframework:spring-jdbc:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Object/Relational Mapping (org.springframework:spring-orm:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring TestContext Framework (org.springframework:spring-test:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Transaction (org.springframework:spring-tx:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Web (org.springframework:spring-web:6.2.8 - https://github.com/spring-projects/spring-framework) - * Spring Web MVC (org.springframework:spring-webmvc:6.2.8 - https://github.com/spring-projects/spring-framework) - * spring-boot (org.springframework.boot:spring-boot:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter (org.springframework.boot:spring-boot-starter:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-thymeleaf (org.springframework.boot:spring-boot-starter-thymeleaf:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-test (org.springframework.boot:spring-boot-test:3.5.3 - https://spring.io/projects/spring-boot) - * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:3.5.3 - https://spring.io/projects/spring-boot) - * Spring Data Core (org.springframework.data:spring-data-commons:3.5.1 - https://spring.io/projects/spring-data) - * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:4.5.1 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-core) - * Spring Data REST - WebMVC (org.springframework.data:spring-data-rest-webmvc:4.5.1 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc) + * Spring AOP (org.springframework:spring-aop:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Beans (org.springframework:spring-beans:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Context (org.springframework:spring-context:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Context Support (org.springframework:spring-context-support:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Core (org.springframework:spring-core:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Commons Logging Bridge (org.springframework:spring-jcl:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring JDBC (org.springframework:spring-jdbc:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Object/Relational Mapping (org.springframework:spring-orm:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring TestContext Framework (org.springframework:spring-test:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Transaction (org.springframework:spring-tx:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Web (org.springframework:spring-web:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring Web MVC (org.springframework:spring-webmvc:6.2.14 - https://github.com/spring-projects/spring-framework) + * spring-boot (org.springframework.boot:spring-boot:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter (org.springframework.boot:spring-boot-starter:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-thymeleaf (org.springframework.boot:spring-boot-starter-thymeleaf:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-test (org.springframework.boot:spring-boot-test:3.5.8 - https://spring.io/projects/spring-boot) + * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:3.5.8 - https://spring.io/projects/spring-boot) + * Spring Data Core (org.springframework.data:spring-data-commons:3.5.6 - https://spring.io/projects/spring-data) + * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:4.5.6 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-core) + * Spring Data REST - WebMVC (org.springframework.data:spring-data-rest-webmvc:4.5.6 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc) * Spring HATEOAS (org.springframework.hateoas:spring-hateoas:2.5.1 - https://github.com/spring-projects/spring-hateoas) * Spring Plugin - Core (org.springframework.plugin:spring-plugin-core:3.0.0 - https://github.com/spring-projects/spring-plugin/spring-plugin-core) - * spring-security-config (org.springframework.security:spring-security-config:6.5.1 - https://spring.io/projects/spring-security) - * spring-security-core (org.springframework.security:spring-security-core:6.5.1 - https://spring.io/projects/spring-security) - * spring-security-crypto (org.springframework.security:spring-security-crypto:6.5.1 - https://spring.io/projects/spring-security) - * spring-security-test (org.springframework.security:spring-security-test:6.5.1 - https://spring.io/projects/spring-security) - * spring-security-web (org.springframework.security:spring-security-web:6.5.1 - https://spring.io/projects/spring-security) + * spring-security-config (org.springframework.security:spring-security-config:6.5.7 - https://spring.io/projects/spring-security) + * spring-security-core (org.springframework.security:spring-security-core:6.5.7 - https://spring.io/projects/spring-security) + * spring-security-crypto (org.springframework.security:spring-security-crypto:6.5.7 - https://spring.io/projects/spring-security) + * spring-security-test (org.springframework.security:spring-security-test:6.5.7 - https://spring.io/projects/spring-security) + * spring-security-web (org.springframework.security:spring-security-web:6.5.7 - https://spring.io/projects/spring-security) * thymeleaf (org.thymeleaf:thymeleaf:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf) * thymeleaf-spring6 (org.thymeleaf:thymeleaf-spring6:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf-spring6) * unbescape (org.unbescape:unbescape:1.1.6.RELEASE - http://www.unbescape.org) * snappy-java (org.xerial.snappy:snappy-java:1.1.10.1 - https://github.com/xerial/snappy-java) * xml-matchers (org.xmlmatchers:xml-matchers:0.10 - http://code.google.com/p/xml-matchers/) - * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.2 - https://www.xmlunit.org/) + * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.10.4 - https://www.xmlunit.org/) + * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.11.0 - https://www.xmlunit.org/) * org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.9.1 - https://www.xmlunit.org/xmlunit-placeholders/) * SnakeYAML (org.yaml:snakeyaml:2.4 - https://bitbucket.org/snakeyaml/snakeyaml) + * AWS Java SDK :: Annotations (software.amazon.awssdk:annotations:2.38.8 - https://aws.amazon.com/sdkforjava/core/annotations) + * AWS Java SDK :: Arns (software.amazon.awssdk:arns:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Auth (software.amazon.awssdk:auth:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: AWS Core (software.amazon.awssdk:aws-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: AWS Query Protocol (software.amazon.awssdk:aws-query-protocol:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: AWS Xml Protocol (software.amazon.awssdk:aws-xml-protocol:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Checksums (software.amazon.awssdk:checksums:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Checksums SPI (software.amazon.awssdk:checksums-spi:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: AWS CRT Core (software.amazon.awssdk:crt-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Endpoints SPI (software.amazon.awssdk:endpoints-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/endpoints-spi) + * AWS Java SDK :: HTTP Auth (software.amazon.awssdk:http-auth:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth AWS (software.amazon.awssdk:http-auth-aws:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth Event Stream (software.amazon.awssdk:http-auth-aws-eventstream:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth SPI (software.amazon.awssdk:http-auth-spi:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Client Interface (software.amazon.awssdk:http-client-spi:2.38.8 - https://aws.amazon.com/sdkforjava/http-client-spi) + * AWS Java SDK :: Identity SPI (software.amazon.awssdk:identity-spi:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: Json Utils (software.amazon.awssdk:json-utils:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Metrics SPI (software.amazon.awssdk:metrics-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/metrics-spi) + * AWS Java SDK :: Profiles (software.amazon.awssdk:profiles:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: Protocol Core (software.amazon.awssdk:protocol-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Regions (software.amazon.awssdk:regions:2.38.8 - https://aws.amazon.com/sdkforjava/core/regions) + * AWS Java SDK :: Retries (software.amazon.awssdk:retries:2.38.8 - https://aws.amazon.com/sdkforjava/core/retries) + * AWS Java SDK :: Retries API (software.amazon.awssdk:retries-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/retries-spi) + * AWS Java SDK :: Services :: Amazon S3 (software.amazon.awssdk:s3:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: SDK Core (software.amazon.awssdk:sdk-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Third Party :: Jackson-core (software.amazon.awssdk:third-party-jackson-core:2.38.8 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Utilities (software.amazon.awssdk:utils:2.38.8 - https://aws.amazon.com/sdkforjava/utils) + * AWS Java SDK :: Utils Lite (software.amazon.awssdk:utils-lite:2.38.8 - https://aws.amazon.com/sdkforjava) + * software.amazon.awssdk.crt:aws-crt (software.amazon.awssdk.crt:aws-crt:0.39.4 - https://github.com/awslabs/aws-crt-java) + * AWS Event Stream (software.amazon.eventstream:eventstream:1.0.1 - https://github.com/awslabs/aws-eventstream-java) * Xerces2-j (xerces:xercesImpl:2.12.2 - https://xerces.apache.org/xerces2-j/) BSD License: @@ -480,8 +492,8 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * janino (org.codehaus.janino:janino:3.1.8 - http://janino-compiler.github.io/janino/) * Stax2 API (org.codehaus.woodstox:stax2-api:4.2.1 - http://github.com/FasterXML/stax2-api) * Hamcrest Date (org.exparity:hamcrest-date:2.0.8 - https://github.com/exparity/hamcrest-date) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) * Hamcrest (org.hamcrest:hamcrest:2.2 - http://hamcrest.org/JavaHamcrest/) * Hamcrest Core (org.hamcrest:hamcrest-core:2.2 - http://hamcrest.org/JavaHamcrest/) @@ -491,23 +503,19 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * asm-analysis (org.ow2.asm:asm-analysis:8.0.1 - http://asm.ow2.io/) * asm-commons (org.ow2.asm:asm-commons:8.0.1 - http://asm.ow2.io/) * asm-tree (org.ow2.asm:asm-tree:8.0.1 - http://asm.ow2.io/) - * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.7 - https://jdbc.postgresql.org) + * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.8 - https://jdbc.postgresql.org) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) * JMatIO (org.tallison:jmatio:1.5 - https://github.com/tballison/jmatio) * XZ for Java (org.tukaani:xz:1.10 - https://tukaani.org/xz/java.html) * XMLUnit for Java (xmlunit:xmlunit:1.3 - http://xmlunit.sourceforge.net/) - CC0: - - * reactive-streams (org.reactivestreams:reactive-streams:1.0.2 - http://www.reactive-streams.org/) - Common Development and Distribution License (CDDL): * JavaMail API (no providers) (com.sun.mail:mailapi:1.6.2 - http://javaee.github.io/javamail/mailapi) * Old JAXB Core (com.sun.xml.bind:jaxb-core:2.3.0.1 - http://jaxb.java.net/jaxb-bundles/jaxb-core) * Old JAXB Runtime (com.sun.xml.bind:jaxb-impl:2.3.1 - http://jaxb.java.net/jaxb-bundles/jaxb-impl) * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) - * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) + * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.5 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.1.0 - https://projects.eclipse.org/projects/ee4j.servlet) * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta) * JavaBeans Activation Framework API jar (javax.activation:javax.activation-api:1.2.0 - http://java.net/all/javax.activation-api/) @@ -516,14 +524,14 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * javax.transaction API (javax.transaction:javax.transaction-api:1.3 - http://jta-spec.java.net) * jaxb-api (javax.xml.bind:jaxb-api:2.3.1 - https://github.com/javaee/jaxb-spec/jaxb-api) * JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight) - * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) + * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) * HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) * OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) Cordra (Version 2) License Agreement: @@ -538,17 +546,17 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Eclipse Distribution License, Version 1.0: * istack common utility code runtime (com.sun.istack:istack-commons-runtime:4.1.2 - https://projects.eclipse.org/projects/ee4j/istack-commons/istack-commons-runtime) - * Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.3 - https://github.com/jakartaee/jaf-api) - * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) + * Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.4 - https://github.com/jakartaee/jaf-api) + * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.5 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) * Jakarta Persistence API (jakarta.persistence:jakarta.persistence-api:3.1.0 - https://github.com/eclipse-ee4j/jpa-api) - * Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.2 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) - * Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.2 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) - * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) - * JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) - * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) - * TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.5 - https://eclipse-ee4j.github.io/jaxb-ri/) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.4 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) + * Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.3 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) + * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) + * JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.6 - https://eclipse-ee4j.github.io/jaxb-ri/) + * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.6 - https://eclipse-ee4j.github.io/jaxb-ri/) + * TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.6 - https://eclipse-ee4j.github.io/jaxb-ri/) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) * MIME streaming extension (org.jvnet.mimepull:mimepull:1.9.15 - https://github.com/eclipse-ee4j/metro-mimepull) * org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core) @@ -557,16 +565,16 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Eclipse Public License: * System Rules (com.github.stefanbirkner:system-rules:1.19.0 - http://stefanbirkner.github.io/system-rules/) - * H2 Database Engine (com.h2database:h2:2.3.232 - https://h2database.com) + * H2 Database Engine (com.h2database:h2:2.4.240 - https://h2database.com) * Jakarta Annotations API (jakarta.annotation:jakarta.annotation-api:2.1.1 - https://projects.eclipse.org/projects/ee4j.ca) - * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.3 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) + * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.5 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) * Jakarta Persistence API (jakarta.persistence:jakarta.persistence-api:3.1.0 - https://github.com/eclipse-ee4j/jpa-api) * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.1.0 - https://projects.eclipse.org/projects/ee4j.servlet) * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta) * Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:3.1.0 - https://github.com/eclipse-ee4j/jaxrs-api) * JUnit (junit:junit:4.13.2 - http://junit.org) - * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.24 - https://www.eclipse.org/aspectj/) - * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.3 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) + * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.25 - https://www.eclipse.org/aspectj/) + * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) * Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.15.v20190215 - http://tomcat.apache.org/taglibs/standard/) * Jetty :: ALPN :: Client (org.eclipse.jetty:jetty-alpn-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-alpn-parent/jetty-alpn-client) @@ -579,27 +587,27 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jetty :: Asynchronous HTTP Client (org.eclipse.jetty:jetty-client:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-client) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Continuation (org.eclipse.jetty:jetty-continuation:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-continuation) - * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.57.v20241219 - https://jetty.org/jetty-deploy/) - * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.57.v20241219 - https://jetty.org/jetty-http/) - * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.57.v20241219 - https://jetty.org/jetty-io/) + * Jetty :: Deployers (org.eclipse.jetty:jetty-deploy:9.4.58.v20250814 - https://jetty.org/jetty-deploy/) + * Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.58.v20250814 - https://jetty.org/jetty-http/) + * Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.58.v20250814 - https://jetty.org/jetty-io/) * Jetty :: JMX Management (org.eclipse.jetty:jetty-jmx:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-jmx) * Jetty :: JNDI Naming (org.eclipse.jetty:jetty-jndi:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Plus (org.eclipse.jetty:jetty-plus:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Rewrite Handler (org.eclipse.jetty:jetty-rewrite:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-rewrite) * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-security) - * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.57.v20241219 - https://jetty.org/jetty-security/) - * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.57.v20241219 - https://jetty.org/jetty-server/) - * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.57.v20241219 - https://jetty.org/jetty-servlet/) + * Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.58.v20250814 - https://jetty.org/jetty-security/) + * Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.58.v20250814 - https://jetty.org/jetty-server/) + * Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.58.v20250814 - https://jetty.org/jetty-servlet/) * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-servlets) - * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.57.v20241219 - https://jetty.org/jetty-util/) - * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.57.v20241219 - https://jetty.org/jetty-util-ajax/) - * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.57.v20241219 - https://jetty.org/jetty-webapp/) + * Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.58.v20250814 - https://jetty.org/jetty-util/) + * Jetty :: Utilities :: Ajax(JSON) (org.eclipse.jetty:jetty-util-ajax:9.4.58.v20250814 - https://jetty.org/jetty-util-ajax/) + * Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.58.v20250814 - https://jetty.org/jetty-webapp/) * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.53.v20231009 - https://eclipse.org/jetty/jetty-xml) - * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.57.v20241219 - https://jetty.org/jetty-xml/) + * Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.58.v20250814 - https://jetty.org/jetty-xml/) * Jetty :: ALPN :: API (org.eclipse.jetty.alpn:alpn-api:1.1.3.v20160715 - http://www.eclipse.org/jetty/alpn-api) * Jetty :: HTTP2 :: Client (org.eclipse.jetty.http2:http2-client:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-client) - * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.57.v20241219 - https://jetty.org/http2-parent/http2-common/) + * Jetty :: HTTP2 :: Common (org.eclipse.jetty.http2:http2-common:9.4.58.v20250814 - https://jetty.org/http2-parent/http2-common/) * Jetty :: HTTP2 :: HPACK (org.eclipse.jetty.http2:http2-hpack:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-hpack) * Jetty :: HTTP2 :: HTTP Client Transport (org.eclipse.jetty.http2:http2-http-client-transport:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-http-client-transport) * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.15.v20190215 - https://eclipse.org/jetty/http2-parent/http2-server) @@ -611,13 +619,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-utils) * OSGi resource locator (org.glassfish.hk2:osgi-resource-locator:1.0.3 - https://projects.eclipse.org/projects/ee4j/osgi-resource-locator) * aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/external/aopalliance-repackaged) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) - * JUnit Platform Commons (org.junit.platform:junit-platform-commons:1.11.4 - https://junit.org/junit5/) - * JUnit Platform Engine API (org.junit.platform:junit-platform-engine:1.11.4 - https://junit.org/junit5/) - * JUnit Vintage Engine (org.junit.vintage:junit-vintage-engine:5.11.4 - https://junit.org/junit5/) * org.locationtech.jts:jts-core (org.locationtech.jts:jts-core:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-core) * org.locationtech.jts.io:jts-io-common (org.locationtech.jts.io:jts-io-common:1.19.0 - https://www.locationtech.org/projects/technology.jts/jts-modules/jts-io/jts-io-common) @@ -642,9 +647,9 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * FindBugs-Annotations (com.google.code.findbugs:annotations:3.0.1u2 - http://findbugs.sourceforge.net/) * JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight) * Hibernate Commons Annotations (org.hibernate.common:hibernate-commons-annotations:6.0.6.Final - http://hibernate.org) - * Hibernate ORM - hibernate-core (org.hibernate.orm:hibernate-core:6.4.8.Final - https://hibernate.org/orm) - * Hibernate ORM - hibernate-jcache (org.hibernate.orm:hibernate-jcache:6.4.8.Final - https://hibernate.org/orm) - * Hibernate ORM - hibernate-jpamodelgen (org.hibernate.orm:hibernate-jpamodelgen:6.4.8.Final - https://hibernate.org/orm) + * Hibernate ORM - hibernate-core (org.hibernate.orm:hibernate-core:6.4.10.Final - https://hibernate.org/orm) + * Hibernate ORM - hibernate-jcache (org.hibernate.orm:hibernate-jcache:6.4.10.Final - https://hibernate.org/orm) + * Hibernate ORM - hibernate-jpamodelgen (org.hibernate.orm:hibernate-jpamodelgen:6.4.10.Final - https://hibernate.org/orm) * im4java (org.im4java:im4java:1.4.0 - http://sourceforge.net/projects/im4java/) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) * XOM (xom:xom:1.3.9 - https://xom.nu) @@ -661,28 +666,33 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Simple Magic (com.j256.simplemagic:simplemagic:1.17 - https://256stuff.com/sources/simplemagic/) + LGPL-2.1-or-later: + + * Java Native Access (net.java.dev.jna:jna:5.13.0 - https://github.com/java-native-access/jna) + MIT License: * dexx (com.github.andrewoma.dexx:collection:0.7 - https://github.com/andrewoma/dexx) - * better-files (com.github.pathikrit:better-files_2.13:3.9.1 - https://github.com/pathikrit/better-files) * Java SemVer (com.github.zafarkhaja:java-semver:0.9.0 - https://github.com/zafarkhaja/jsemver) * dd-plist (com.googlecode.plist:dd-plist:1.28 - http://www.github.com/3breadt/dd-plist) * DigitalCollections: IIIF API Library (de.digitalcollections.iiif:iiif-apis:0.3.11 - https://github.com/dbmdz/iiif-apis) - * s3mock (io.findify:s3mock_2.13:0.2.6 - https://github.com/findify/s3mock) * ClassGraph (io.github.classgraph:classgraph:4.8.165 - https://github.com/classgraph/classgraph) * JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) - * Bouncy Castle JavaMail S/MIME APIs (org.bouncycastle:bcmail-jdk18on:1.80 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle JavaMail Jakarta S/MIME APIs (org.bouncycastle:bcjmail-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) * org.brotli:dec (org.brotli:dec:0.1.2 - http://brotli.org/dec) - * Checker Qual (org.checkerframework:checker-qual:3.49.5 - https://checkerframework.org/) - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * Checker Qual (org.checkerframework:checker-qual:3.52.0 - https://checkerframework.org/) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) + * jsoup Java HTML Parser (org.jsoup:jsoup:1.21.2 - https://jsoup.org/) * mockito-core (org.mockito:mockito-core:3.12.4 - https://github.com/mockito/mockito) * mockito-inline (org.mockito:mockito-inline:3.12.4 - https://github.com/mockito/mockito) + * Duct Tape (org.rnorth.duct-tape:duct-tape:1.0.8 - https://github.com/rnorth/duct-tape) * SLF4J API Module (org.slf4j:slf4j-api:2.0.17 - http://www.slf4j.org) + * Testcontainers Core (org.testcontainers:testcontainers:1.21.3 - https://java.testcontainers.org) * HAL Browser (org.webjars:hal-browser:ad9b865 - http://webjars.org) * toastr (org.webjars.bowergithub.codeseven:toastr:2.1.4 - http://webjars.org) * backbone (org.webjars.bowergithub.jashkenas:backbone:1.4.1 - https://www.webjars.org) @@ -690,28 +700,36 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * jquery (org.webjars.bowergithub.jquery:jquery-dist:3.7.1 - https://www.webjars.org) * urijs (org.webjars.bowergithub.medialize:uri.js:1.19.11 - https://www.webjars.org) * bootstrap (org.webjars.bowergithub.twbs:bootstrap:4.6.2 - https://www.webjars.org) - * core-js (org.webjars.npm:core-js:3.42.0 - https://www.webjars.org) + * core-js (org.webjars.npm:core-js:3.46.0 - https://www.webjars.org) * @json-editor/json-editor (org.webjars.npm:json-editor__json-editor:2.15.2 - https://www.webjars.org) + MIT-0: + + * reactive-streams (org.reactivestreams:reactive-streams:1.0.4 - http://www.reactive-streams.org/) + Mozilla Public License: * juniversalchardet (com.github.albfernandez:juniversalchardet:2.5.0 - https://github.com/albfernandez/juniversalchardet) - * H2 Database Engine (com.h2database:h2:2.3.232 - https://h2database.com) + * H2 Database Engine (com.h2database:h2:2.4.240 - https://h2database.com) * Saxon-HE (net.sf.saxon:Saxon-HE:9.9.1-8 - http://www.saxonica.com/) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) * Mozilla Rhino (org.mozilla:rhino:1.7.7.2 - https://developer.mozilla.org/en/Rhino) Public Domain: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-core-common (org.glassfish.jersey.core:jersey-common:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-common) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) * HdrHistogram (org.hdrhistogram:HdrHistogram:2.2.2 - http://hdrhistogram.github.io/HdrHistogram/) * JSON in Java (org.json:json:20231013 - https://github.com/douglascrockford/JSON-java) * LatencyUtils (org.latencyutils:LatencyUtils:2.0.3 - http://latencyutils.github.io/LatencyUtils/) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) + The Apache Software License, version 2.0: + + * picocli (info.picocli:picocli:4.7.6 - https://picocli.info) + UnRar License: * Java Unrar (com.github.junrar:junrar:7.5.5 - https://github.com/junrar/junrar) @@ -722,12 +740,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines W3C license: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) jQuery license: - * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) - * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.10 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) + * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) + * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) From 7e59b972518eff274659b6549283159e64d38aa3 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 17 Dec 2025 11:12:25 -0600 Subject: [PATCH 480/701] [maven-release-plugin] prepare release dspace-7.6.6 --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-rest/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/rest/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 32 ++++++++++++++++---------------- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index e0baedbcd8cf..46c251fc7d78 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 9cc05dc5ceb9..0f1e665002db 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index c9211d5e592d..df82bff7c4d5 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index fc40a39d2a2e..1fdc43af6319 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index fa5c56003bf5..30a097bcd30a 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - 7.6.6-SNAPSHOT + 7.6.6 DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 0f03a7335c24..4c9bebcdd419 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 26f6ecff3ef5..716302433d45 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 15d918c4d106..0585d2bbeed3 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 4972cd4e98d4..23cf37aa9016 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 41274052cd7c..06b8244490e9 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index ecc5b3fec7a3..69d54221cefe 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index 4e7adb7b9224..7ed7268debac 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 0e33182c698c..919f687c97fb 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - 7.6.6-SNAPSHOT + 7.6.6 .. diff --git a/dspace/pom.xml b/dspace/pom.xml index 6338d764b515..9ca0c13d4283 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 7.6.6-SNAPSHOT + 7.6.6 ../pom.xml diff --git a/pom.xml b/pom.xml index eaaf4d9d017d..1ba81c6b1fb3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 7.6.6-SNAPSHOT + 7.6.6 DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -875,14 +875,14 @@ org.dspace dspace-rest - 7.6.6-SNAPSHOT + 7.6.6 jar classes org.dspace dspace-rest - 7.6.6-SNAPSHOT + 7.6.6 war @@ -1031,69 +1031,69 @@ org.dspace dspace-api - 7.6.6-SNAPSHOT + 7.6.6 org.dspace dspace-api test-jar - 7.6.6-SNAPSHOT + 7.6.6 test org.dspace.modules additions - 7.6.6-SNAPSHOT + 7.6.6 org.dspace dspace-sword - 7.6.6-SNAPSHOT + 7.6.6 org.dspace dspace-swordv2 - 7.6.6-SNAPSHOT + 7.6.6 org.dspace dspace-oai - 7.6.6-SNAPSHOT + 7.6.6 org.dspace dspace-services - 7.6.6-SNAPSHOT + 7.6.6 org.dspace dspace-server-webapp test-jar - 7.6.6-SNAPSHOT + 7.6.6 test org.dspace dspace-rdf - 7.6.6-SNAPSHOT + 7.6.6 org.dspace dspace-iiif - 7.6.6-SNAPSHOT + 7.6.6 org.dspace dspace-server-webapp - 7.6.6-SNAPSHOT + 7.6.6 jar classes org.dspace dspace-server-webapp - 7.6.6-SNAPSHOT + 7.6.6 war @@ -1939,7 +1939,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-7_x + dspace-7.6.6 From 2f3fcaa3d7eb7df5c9f6d39cd3782e6b3e09b583 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 17 Dec 2025 11:55:27 -0600 Subject: [PATCH 481/701] Revert bouncycastle to version that is compatible with maven-gpg-plugin. --- pom.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d4e45b05cde5..0d8fef4d2db5 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,9 @@ 2.0.17 3.2.3 - 1.82 + + 1.81 8.0.1 3.1.11 From b9396131728bbac686b0cf950e0a863ae3a0ea2e Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 17 Dec 2025 13:57:10 -0600 Subject: [PATCH 482/701] [maven-release-plugin] prepare release dspace-8.3 --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 6 +++++- dspace/modules/pom.xml | 2 +- dspace/modules/server-boot/pom.xml | 6 +++++- dspace/modules/server/pom.xml | 6 +++++- dspace/pom.xml | 2 +- pom.xml | 28 ++++++++++++++-------------- 14 files changed, 39 insertions(+), 27 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 09cbd88da277..6e061919362b 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 6977ca4fd0ff..968c2678b7bc 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 407711f680aa..01bdb90c2285 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 8.3-SNAPSHOT + 8.3 .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 5bc1c402bd7d..180b7844c210 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 52b68c89bdaa..adba3efc08d0 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -14,7 +14,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 8baf523a3b19..2c129e02b1f2 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 00369160636e..efed58a13cee 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index dc48a2759a82..d0a7df571e36 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index a82710f88c99..d2e2b775addf 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 8.3-SNAPSHOT + 8.3 .. @@ -296,4 +296,8 @@ + + + dspace-8.3 + diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 7800d80026d9..df8ffceb3f03 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 ../../pom.xml diff --git a/dspace/modules/server-boot/pom.xml b/dspace/modules/server-boot/pom.xml index 6b73cc459dd3..24dc21ae0f2a 100644 --- a/dspace/modules/server-boot/pom.xml +++ b/dspace/modules/server-boot/pom.xml @@ -11,7 +11,7 @@ modules org.dspace - 8.3-SNAPSHOT + 8.3 .. @@ -121,4 +121,8 @@ + + + dspace-8.3 + diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 48ecdfd0274b..48a5acfdc10b 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -7,7 +7,7 @@ modules org.dspace - 8.3-SNAPSHOT + 8.3 .. @@ -341,4 +341,8 @@ + + + dspace-8.3 + diff --git a/dspace/pom.xml b/dspace/pom.xml index 571ff5e371f0..1ab977bb4f1b 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 8.3-SNAPSHOT + 8.3 ../pom.xml diff --git a/pom.xml b/pom.xml index 0d8fef4d2db5..f62b10b11588 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 8.3-SNAPSHOT + 8.3 DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -1018,68 +1018,68 @@ org.dspace dspace-api - 8.3-SNAPSHOT + 8.3 org.dspace dspace-api test-jar - 8.3-SNAPSHOT + 8.3 test org.dspace.modules additions - 8.3-SNAPSHOT + 8.3 org.dspace.modules server classes - 8.3-SNAPSHOT + 8.3 org.dspace dspace-sword - 8.3-SNAPSHOT + 8.3 org.dspace dspace-swordv2 - 8.3-SNAPSHOT + 8.3 org.dspace dspace-oai - 8.3-SNAPSHOT + 8.3 org.dspace dspace-services - 8.3-SNAPSHOT + 8.3 org.dspace dspace-server-webapp test-jar - 8.3-SNAPSHOT + 8.3 test org.dspace dspace-rdf - 8.3-SNAPSHOT + 8.3 org.dspace dspace-iiif - 8.3-SNAPSHOT + 8.3 org.dspace dspace-server-webapp - 8.3-SNAPSHOT + 8.3 @@ -1912,7 +1912,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-8_x + dspace-8.3 From 004c2c0e23ff2e1eedbaecd60c2fc00ea450a12b Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 17 Dec 2025 13:57:15 -0600 Subject: [PATCH 483/701] [maven-release-plugin] prepare for next development iteration --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 7 +------ dspace/modules/pom.xml | 2 +- dspace/modules/server-boot/pom.xml | 7 +------ dspace/modules/server/pom.xml | 7 +------ dspace/pom.xml | 2 +- pom.xml | 28 ++++++++++++++-------------- 14 files changed, 27 insertions(+), 42 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 6e061919362b..aaf493d6f2f8 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 968c2678b7bc..f84acfe5a247 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 01bdb90c2285..baf8f1616acd 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 8.3 + 8.4-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 180b7844c210..8af62625ae27 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index adba3efc08d0..4b050d9f492e 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -14,7 +14,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 2c129e02b1f2..5c3ff226c22c 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index efed58a13cee..d1d625d4f576 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index d0a7df571e36..a6ce18ed6a93 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index d2e2b775addf..b1da55b508c7 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 8.3 + 8.4-SNAPSHOT .. @@ -295,9 +295,4 @@ - - - - dspace-8.3 - diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index df8ffceb3f03..f44aa1925006 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/server-boot/pom.xml b/dspace/modules/server-boot/pom.xml index 24dc21ae0f2a..e95360e59827 100644 --- a/dspace/modules/server-boot/pom.xml +++ b/dspace/modules/server-boot/pom.xml @@ -11,7 +11,7 @@ modules org.dspace - 8.3 + 8.4-SNAPSHOT .. @@ -120,9 +120,4 @@ - - - - dspace-8.3 - diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 48a5acfdc10b..db11e7f46f57 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -7,7 +7,7 @@ modules org.dspace - 8.3 + 8.4-SNAPSHOT .. @@ -340,9 +340,4 @@ - - - - dspace-8.3 - diff --git a/dspace/pom.xml b/dspace/pom.xml index 1ab977bb4f1b..dc4b27aa65ff 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 8.3 + 8.4-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index f62b10b11588..d32537659208 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 8.3 + 8.4-SNAPSHOT DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -1018,68 +1018,68 @@ org.dspace dspace-api - 8.3 + 8.4-SNAPSHOT org.dspace dspace-api test-jar - 8.3 + 8.4-SNAPSHOT test org.dspace.modules additions - 8.3 + 8.4-SNAPSHOT org.dspace.modules server classes - 8.3 + 8.4-SNAPSHOT org.dspace dspace-sword - 8.3 + 8.4-SNAPSHOT org.dspace dspace-swordv2 - 8.3 + 8.4-SNAPSHOT org.dspace dspace-oai - 8.3 + 8.4-SNAPSHOT org.dspace dspace-services - 8.3 + 8.4-SNAPSHOT org.dspace dspace-server-webapp test-jar - 8.3 + 8.4-SNAPSHOT test org.dspace dspace-rdf - 8.3 + 8.4-SNAPSHOT org.dspace dspace-iiif - 8.3 + 8.4-SNAPSHOT org.dspace dspace-server-webapp - 8.3 + 8.4-SNAPSHOT @@ -1912,7 +1912,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-8.3 + dspace-8_x From 67329253a341153768616819cf61fa4158088406 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 23:24:08 +0000 Subject: [PATCH 484/701] build(deps): bump pdfbox-version from 3.0.5 to 3.0.6 Bumps `pdfbox-version` from 3.0.5 to 3.0.6. Updates `org.apache.pdfbox:pdfbox` from 3.0.5 to 3.0.6 Updates `org.apache.pdfbox:fontbox` from 3.0.5 to 3.0.6 --- updated-dependencies: - dependency-name: org.apache.pdfbox:pdfbox dependency-version: 3.0.6 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.pdfbox:fontbox dependency-version: 3.0.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d32537659208..f834970df059 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ 9.4.58.v20250814 2.25.2 - 3.0.5 + 3.0.6 1.19.0 2.0.17 3.2.3 From 77f17232be7565ba200e7376f11faf63cf6a4867 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 16:26:44 -0500 Subject: [PATCH 485/701] Avoid using "--dspace.dir" flag for Spring Boot startup. Instead, specify "dspace__P__dir" environment variable. (cherry picked from commit 4ee096199150fb9371fb2ec3061d861b247c24bb) --- Dockerfile | 14 ++++++++------ Dockerfile.test | 14 ++++++++------ docker-compose.yml | 7 +++---- dspace/src/main/docker-compose/db.entities.yml | 2 +- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index eb90299ccd7d..63b6af22682a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,11 +55,13 @@ RUN ant init_installation update_configs update_code update_webapps # Step 3 - Start up DSpace via Runnable JAR FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL -WORKDIR $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Need host command for "[dspace]/bin/make-handle-config" RUN apt-get update \ && apt-get install -y --no-install-recommends host \ @@ -69,5 +71,5 @@ RUN apt-get update \ EXPOSE 8080 8000 # Give java extra memory (2GB) ENV JAVA_OPTS=-Xmx2000m -# On startup, run DSpace Runnable JAR -ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"] +# On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) +ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"] diff --git a/Dockerfile.test b/Dockerfile.test index c9627e439fd7..220b15f85c19 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -54,11 +54,13 @@ RUN ant init_installation update_configs update_code update_webapps # Step 3 - Start up DSpace via Runnable JAR FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL -WORKDIR $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Need host command for "[dspace]/bin/make-handle-config" RUN apt-get update \ && apt-get install -y --no-install-recommends host \ @@ -70,5 +72,5 @@ EXPOSE 8080 8000 ENV JAVA_OPTS=-Xmx2000m # enable JVM debugging via JDWP ENV JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000 -# On startup, run DSpace Runnable JAR -ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"] +# On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) +ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"] diff --git a/docker-compose.yml b/docker-compose.yml index 9177ff4bd977..26966929bf53 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,9 +14,8 @@ services: # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml # __P__ => "." (e.g. dspace__P__dir => dspace.dir) # __D__ => "-" (e.g. google__D__metadata => google-metadata) - # dspace.dir: Must match with Dockerfile's DSPACE_INSTALL directory. - dspace__P__dir: /dspace - # Uncomment to set a non-default value for dspace.server.url or dspace.ui.url + # Uncomment to set a non-default value for dspace.dir, dspace.server.url or dspace.ui.url + # dspace__P__dir: /dspace # dspace__P__server__P__url: http://localhost:8080/server # dspace__P__ui__P__url: http://localhost:4000 dspace__P__name: 'DSpace Started with Docker Compose' @@ -59,7 +58,7 @@ services: - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate - java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace + java -jar /dspace/webapps/server-boot.jar # DSpace PostgreSQL database container dspacedb: container_name: dspacedb diff --git a/dspace/src/main/docker-compose/db.entities.yml b/dspace/src/main/docker-compose/db.entities.yml index 243511ddae3b..6fb6e486282f 100644 --- a/dspace/src/main/docker-compose/db.entities.yml +++ b/dspace/src/main/docker-compose/db.entities.yml @@ -24,4 +24,4 @@ services: - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate ignored - java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace + java -jar /dspace/webapps/server-boot.jar From 6e07c96d8c85f37dc8de78d8aa4c4a0d76604a42 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 Aug 2025 16:27:53 -0500 Subject: [PATCH 486/701] Move entrypoint & command to Dockerfile.cli. Update it to use "dspace__P__dir" environment variable. (cherry picked from commit 8b1c9e0669d6e37495aa4d303b544f0901d1b27b) --- Dockerfile.cli | 14 +++++++++++--- docker-compose-cli.yml | 4 ---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Dockerfile.cli b/Dockerfile.cli index 1f08357b3bba..d67c7644fd59 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -49,10 +49,13 @@ RUN ant init_installation update_configs update_code # Step 3 - Run jdk FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Give java extra memory (1GB) ENV JAVA_OPTS=-Xmx1000m # Install unzip for AIPs @@ -60,3 +63,8 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends unzip \ && apt-get purge -y --auto-remove \ && rm -rf /var/lib/apt/lists/* + +# On startup, run DSpace commandline script +ENTRYPOINT ["./bin/dspace"] +# By default just pass 'help' command to ./bin/dspace +CMD ["help"] diff --git a/docker-compose-cli.yml b/docker-compose-cli.yml index 5d15845fa8df..ee567bcf79c4 100644 --- a/docker-compose-cli.yml +++ b/docker-compose-cli.yml @@ -16,8 +16,6 @@ services: # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml # __P__ => "." (e.g. dspace__P__dir => dspace.dir) # __D__ => "-" (e.g. google__D__metadata => google-metadata) - # dspace.dir: Must match with Dockerfile's DSPACE_INSTALL directory. - dspace__P__dir: /dspace # db.url: Ensure we are using the 'dspacedb' image for our database db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' # solr.server: Ensure we are using the 'dspacesolr' image for Solr @@ -28,8 +26,6 @@ services: # Mount local [src]/dspace/config/ to container. This syncs your local configs with container # NOTE: Environment variables specified above will OVERRIDE any configs in local.cfg or dspace.cfg - ./dspace/config:/dspace/config - entrypoint: /dspace/bin/dspace - command: help tty: true stdin_open: true From 76147e5f12afa5a28736e8c50720d77d79b98981 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 12 Jan 2026 16:42:01 -0600 Subject: [PATCH 487/701] Remove unnecessary tty/stdin_open settings from several Docker Compose scripts --- docker-compose-cli.yml | 2 -- docker-compose.yml | 6 ------ dspace/src/main/docker-compose/docker-compose-angular.yml | 2 -- .../src/main/docker-compose/docker-compose-shibboleth.yml | 2 -- 4 files changed, 12 deletions(-) diff --git a/docker-compose-cli.yml b/docker-compose-cli.yml index ee567bcf79c4..564759fcf375 100644 --- a/docker-compose-cli.yml +++ b/docker-compose-cli.yml @@ -26,8 +26,6 @@ services: # Mount local [src]/dspace/config/ to container. This syncs your local configs with container # NOTE: Environment variables specified above will OVERRIDE any configs in local.cfg or dspace.cfg - ./dspace/config:/dspace/config - tty: true - stdin_open: true volumes: assetstore: diff --git a/docker-compose.yml b/docker-compose.yml index 26966929bf53..9cbdd6a9dc67 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,8 +40,6 @@ services: target: 8080 - published: 8000 target: 8000 - stdin_open: true - tty: true volumes: # Keep DSpace assetstore directory between reboots - assetstore:/dspace/assetstore @@ -75,8 +73,6 @@ services: ports: - published: 5432 target: 5432 - stdin_open: true - tty: true volumes: # Keep Postgres data directory between reboots - pgdata:/pgdata @@ -96,8 +92,6 @@ services: ports: - published: 8983 target: 8983 - stdin_open: true - tty: true working_dir: /var/solr/data volumes: # Keep Solr data directory between reboots diff --git a/dspace/src/main/docker-compose/docker-compose-angular.yml b/dspace/src/main/docker-compose/docker-compose-angular.yml index 3610d23286b8..a88ab893950a 100644 --- a/dspace/src/main/docker-compose/docker-compose-angular.yml +++ b/dspace/src/main/docker-compose/docker-compose-angular.yml @@ -32,5 +32,3 @@ services: target: 4000 - published: 9876 target: 9876 - stdin_open: true - tty: true diff --git a/dspace/src/main/docker-compose/docker-compose-shibboleth.yml b/dspace/src/main/docker-compose/docker-compose-shibboleth.yml index f7fb2dcbd1ae..0b3178b75453 100644 --- a/dspace/src/main/docker-compose/docker-compose-shibboleth.yml +++ b/dspace/src/main/docker-compose/docker-compose-shibboleth.yml @@ -30,8 +30,6 @@ services: target: 80 - published: 443 target: 443 - stdin_open: true - tty: true environment: # Default to using "localhost" for Apache & Shibboleth # However, you can override this via the "DSPACE_HOSTNAME" environment variable. From 74d8f3371192f659357212a15200a8d6b33bfc73 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 12 Jan 2026 16:52:00 -0600 Subject: [PATCH 488/701] Correct several docker compose scripts to use $DOCKER_REGISTRY and $DOCKER_OWNER env variables. --- dspace/src/main/docker-compose/db.entities.yml | 2 +- dspace/src/main/docker-compose/db.restore.yml | 2 +- dspace/src/main/docker-compose/docker-compose-angular.yml | 4 +--- dspace/src/main/docker-compose/docker-compose-shibboleth.yml | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dspace/src/main/docker-compose/db.entities.yml b/dspace/src/main/docker-compose/db.entities.yml index 6fb6e486282f..8b09625f6b77 100644 --- a/dspace/src/main/docker-compose/db.entities.yml +++ b/dspace/src/main/docker-compose/db.entities.yml @@ -8,7 +8,7 @@ services: dspacedb: - image: dspace/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql" environment: # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql diff --git a/dspace/src/main/docker-compose/db.restore.yml b/dspace/src/main/docker-compose/db.restore.yml index dc3993a78cf5..fa34019a2dc0 100644 --- a/dspace/src/main/docker-compose/db.restore.yml +++ b/dspace/src/main/docker-compose/db.restore.yml @@ -12,7 +12,7 @@ # This can be used to restore a "dspacedb" container from a pg_dump, or during upgrade to a new version of PostgreSQL. services: dspacedb: - image: dspace/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql" environment: # Location where the dump SQL file will be available on the running container - LOCALSQL=/tmp/pgdump.sql diff --git a/dspace/src/main/docker-compose/docker-compose-angular.yml b/dspace/src/main/docker-compose/docker-compose-angular.yml index a88ab893950a..12b38343bf15 100644 --- a/dspace/src/main/docker-compose/docker-compose-angular.yml +++ b/dspace/src/main/docker-compose/docker-compose-angular.yml @@ -26,9 +26,7 @@ services: DSPACE_REST_HOST: localhost DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:${DSPACE_VER:-dspace-8_x} + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x}" ports: - published: 4000 target: 4000 - - published: 9876 - target: 9876 diff --git a/dspace/src/main/docker-compose/docker-compose-shibboleth.yml b/dspace/src/main/docker-compose/docker-compose-shibboleth.yml index 0b3178b75453..1290fbc1e909 100644 --- a/dspace/src/main/docker-compose/docker-compose-shibboleth.yml +++ b/dspace/src/main/docker-compose/docker-compose-shibboleth.yml @@ -21,7 +21,7 @@ services: container_name: dspace-shibboleth depends_on: - dspace - image: dspace/dspace-shibboleth + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-shibboleth" build: # Must be relative to root, so that it can be built alongside [src]/docker-compose.yml context: ./dspace/src/main/docker/dspace-shibboleth From d75e09cb760108565d2f45746a58cd0f06f9c1d5 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 12 Jan 2026 16:54:13 -0600 Subject: [PATCH 489/701] Update Angular docker compose script to use production mode (as dev mode is only useful with src code). Requires minor update to backend docker compose script. --- docker-compose.yml | 3 +++ dspace/src/main/docker-compose/docker-compose-angular.yml | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9cbdd6a9dc67..8d73169e4194 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,9 @@ services: # dspace__P__dir: /dspace # dspace__P__server__P__url: http://localhost:8080/server # dspace__P__ui__P__url: http://localhost:4000 + # Set SSR URL to the Docker container name so that UI can contact container directly in Production mode. + # (This is necessary for docker-compose-angular.yml as it uses production mode by default) + dspace__P__server__P__ssr__P__url: http://dspace:8080/server dspace__P__name: 'DSpace Started with Docker Compose' # db.url: Ensure we are using the 'dspacedb' image for our database db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' diff --git a/dspace/src/main/docker-compose/docker-compose-angular.yml b/dspace/src/main/docker-compose/docker-compose-angular.yml index 12b38343bf15..0f76be0bba4d 100644 --- a/dspace/src/main/docker-compose/docker-compose-angular.yml +++ b/dspace/src/main/docker-compose/docker-compose-angular.yml @@ -26,7 +26,9 @@ services: DSPACE_REST_HOST: localhost DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: /server - image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x}" + # Ensure SSR can use the 'dspace' Docker image directly (see docker-compose-rest.yml) + DSPACE_REST_SSRBASEURL: http://dspace:8080/server + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x-dist}" ports: - published: 4000 target: 4000 From 878efcbdc98073897859c6b751ba60616c067352 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 12 Jan 2026 17:03:15 -0600 Subject: [PATCH 490/701] Fix minor networking inconsistencies similar to https://github.com/DSpace/dspace-angular/pull/4987 --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 8d73169e4194..016046e4c585 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,6 +5,8 @@ networks: # Define a custom subnet for our DSpace network, so that we can easily trust requests from host to container. # If you customize this value, be sure to customize the 'proxies.trusted.ipranges' env variable below. - subnet: 172.23.0.0/16 + # Explicitly set external=false because this script creates the network. + external: false services: # DSpace (backend) webapp container dspace: From 33ccc68d7c354c25df0bb508c037fbbadf9bb3c3 Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Sat, 17 Jan 2026 11:16:52 -0300 Subject: [PATCH 491/701] fix: rename RegexPasswordValidatorIT to RegexPasswordValidatorTest (cherry picked from commit 1f4b100be3b8e7b875e38c0d1ac4850e9d208de4) --- ...ordValidatorIT.java => RegexPasswordValidatorTest.java} | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) rename dspace-api/src/test/java/org/dspace/authorize/{RegexPasswordValidatorIT.java => RegexPasswordValidatorTest.java} (92%) diff --git a/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorIT.java b/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java similarity index 92% rename from dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorIT.java rename to dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java index 7286fb8e8374..7a6a1ca4a10f 100644 --- a/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorIT.java +++ b/dspace-api/src/test/java/org/dspace/authorize/RegexPasswordValidatorTest.java @@ -11,22 +11,19 @@ import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.when; -import org.dspace.AbstractIntegrationTest; +import org.dspace.AbstractUnitTest; import org.dspace.services.ConfigurationService; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; /** * Unit tests for {@link RegexPasswordValidator}. * * @author Luca Giamminonni (luca.giamminonni at 4science.it) */ -@RunWith(MockitoJUnitRunner.class) -public class RegexPasswordValidatorIT extends AbstractIntegrationTest { +public class RegexPasswordValidatorTest extends AbstractUnitTest { @Mock private ConfigurationService configurationService; From a07499730aff2e767b0cc62848e71575b3b848e7 Mon Sep 17 00:00:00 2001 From: Drew Heles Date: Wed, 21 Jan 2026 08:50:15 -0500 Subject: [PATCH 492/701] Add DataCite sandbox url to crosswalk (cherry picked from commit 3a9f3b28b2028076d822b413b344acc49aa461e5) --- dspace/config/crosswalks/DIM2DataCite.xsl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace/config/crosswalks/DIM2DataCite.xsl b/dspace/config/crosswalks/DIM2DataCite.xsl index 935b3dc4038a..64b004b063c3 100644 --- a/dspace/config/crosswalks/DIM2DataCite.xsl +++ b/dspace/config/crosswalks/DIM2DataCite.xsl @@ -350,6 +350,9 @@ + + + From 2c55714aa34794458457b8e7efb0b41bfb4ee143 Mon Sep 17 00:00:00 2001 From: soto-raul Date: Mon, 8 Dec 2025 17:28:59 -0700 Subject: [PATCH 493/701] fix: replace deprecated AntPathRequestMatcher with PathPatternRequestMatcher (cherry picked from commit 587dec08e3d6979efb51ac8b183a79636ff0b96e) --- .../dspace/app/rest/security/StatelessLoginFilter.java | 5 +++-- .../app/rest/security/WebSecurityConfiguration.java | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java index caead66df25f..70d731f0b7b2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java @@ -18,11 +18,12 @@ import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.core.Context; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; /** * This class will filter /api/authn/login requests to try and authenticate them. Keep in mind, this filter runs *after* @@ -57,7 +58,7 @@ public void afterPropertiesSet() { public StatelessLoginFilter(String url, String httpMethod, AuthenticationManager authenticationManager, RestAuthenticationService restAuthenticationService) { // NOTE: attemptAuthentication() below will only be triggered by requests that match both this URL and method - super(new AntPathRequestMatcher(url, httpMethod)); + super(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.valueOf(httpMethod),url)); this.authenticationManager = authenticationManager; this.restAuthenticationService = restAuthenticationService; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index f032902a5a3e..bf5e2163f43f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -30,7 +30,7 @@ import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; /** * Spring Security configuration for DSpace Server Webapp @@ -104,7 +104,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.securityMatcher("/api/**", "/iiif/**", actuatorBasePath + "/**", "/signposting/**") .authorizeHttpRequests((requests) -> requests // Ensure /actuator/info endpoint is restricted to admins - .requestMatchers(new AntPathRequestMatcher(actuatorBasePath + "/info", HttpMethod.GET.name())) + .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, + actuatorBasePath + "/info")) .hasAnyAuthority(ADMIN_GRANT) // All other requests should be permitted at this layer because we check permissions on each method // via @PreAuthorize annotations. As this code runs first, we must permitAll() here in order to pass @@ -141,7 +142,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // On logout, clear the "session" salt .addLogoutHandler(customLogoutHandler) // Configure the logout entry point & require POST - .logoutRequestMatcher(new AntPathRequestMatcher("/api/authn/logout", HttpMethod.POST.name())) + .logoutRequestMatcher(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, + "/api/authn/logout")) // When logout is successful, return OK (204) status .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT)) ) From 19e876496022fd3f33b9ef997089619cbb0512dc Mon Sep 17 00:00:00 2001 From: soto-raul Date: Fri, 12 Dec 2025 10:17:34 -0700 Subject: [PATCH 494/701] fix: replaced explicit PathPatternRequestMatcher with HttpSecurity fluent API calls (cherry picked from commit 436e49095022a0636cb15eafce1caf31e46f8923) --- .../app/rest/security/WebSecurityConfiguration.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index bf5e2163f43f..8d0920a2031c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -30,7 +30,6 @@ import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; -import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; /** * Spring Security configuration for DSpace Server Webapp @@ -104,8 +103,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.securityMatcher("/api/**", "/iiif/**", actuatorBasePath + "/**", "/signposting/**") .authorizeHttpRequests((requests) -> requests // Ensure /actuator/info endpoint is restricted to admins - .requestMatchers(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.GET, - actuatorBasePath + "/info")) + .requestMatchers(HttpMethod.GET, actuatorBasePath + "/info") .hasAnyAuthority(ADMIN_GRANT) // All other requests should be permitted at this layer because we check permissions on each method // via @PreAuthorize annotations. As this code runs first, we must permitAll() here in order to pass @@ -142,8 +140,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // On logout, clear the "session" salt .addLogoutHandler(customLogoutHandler) // Configure the logout entry point & require POST - .logoutRequestMatcher(PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST, - "/api/authn/logout")) + // If CSRF protection is enabled (default in DSpace REST), a POST request is needed to trigger logout + .logoutUrl("/api/authn/logout") // When logout is successful, return OK (204) status .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT)) ) From 3e3182c0b334710791239339cbfd659c95b0a16f Mon Sep 17 00:00:00 2001 From: milanmajchrak <90026355+milanmajchrak@users.noreply.github.com> Date: Tue, 15 Apr 2025 11:01:44 +0200 Subject: [PATCH 495/701] ZCU-PUB/The bitstream name is encoded in the URL. Updated the test to test special characters in the URL (#930) (cherry picked from commit 29199d5935ded1f48dbd30727af5530bc6c2ef6c) --- .../src/test/java/org/dspace/app/sword2/Swordv2IT.java | 3 ++- .../src/main/java/org/dspace/sword2/SwordUrlManager.java | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java index 296bef1bce4d..562f7e19edcb 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java @@ -220,7 +220,8 @@ public void depositAndEditViaSwordTest() throws Exception { // Add required headers HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); - headers.setContentDisposition(ContentDisposition.attachment().filename("example.zip").build()); + // Test the file with spaces or special characters in the name + headers.setContentDisposition(ContentDisposition.attachment().filename("example .zip").build()); headers.set("Packaging", "http://purl.org/net/sword/package/METSDSpaceSIP"); headers.setAccept(List.of(MediaType.APPLICATION_ATOM_XML)); diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java index 1d49bc27977c..4ba0f9950e11 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordUrlManager.java @@ -7,6 +7,8 @@ */ package org.dspace.sword2; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.List; @@ -386,10 +388,10 @@ public String getBitstreamUrl(Bitstream bitstream) if (handle != null && !"".equals(handle)) { bsLink = bsLink + "/bitstream/" + handle + "/" + - bitstream.getSequenceID() + "/" + bitstream.getName(); + bitstream.getSequenceID() + "/" + URLEncoder.encode(bitstream.getName(), StandardCharsets.UTF_8); } else { bsLink = bsLink + "/retrieve/" + bitstream.getID() + "/" + - bitstream.getName(); + URLEncoder.encode(bitstream.getName(), StandardCharsets.UTF_8); } return bsLink; @@ -401,7 +403,7 @@ public String getBitstreamUrl(Bitstream bitstream) public String getActionableBitstreamUrl(Bitstream bitstream) throws DSpaceSwordException { return this.getSwordBaseUrl() + "/edit-media/bitstream/" + - bitstream.getID() + "/" + bitstream.getName(); + bitstream.getID() + "/" + URLEncoder.encode(bitstream.getName(), StandardCharsets.UTF_8); } public boolean isActionableBitstreamUrl(Context context, String url) { From 01188efcf194bbb8fa8e8334eabf231caebed892 Mon Sep 17 00:00:00 2001 From: Mark Patton Date: Mon, 15 Dec 2025 11:14:53 -0500 Subject: [PATCH 496/701] Make sure that bitstream downloads have a content-disposition header set (cherry picked from commit 9ae59e7a3e634e3bf71b3643d124ea9a8d625d84) --- .../main/java/org/dspace/app/rest/BitstreamRestController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java index 11b048e23ef1..8659bd982df4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java @@ -160,6 +160,8 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp if ((dispositionThreshold >= 0 && filesize > dispositionThreshold) || checkFormatForContentDisposition(format)) { httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT); + } else { + httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_INLINE); } //We have all the data we need, close the connection to the database so that it doesn't stay open during From 5a0da20b2a685b9c5db1378ce3d7b6bf0a21c587 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:37:04 +0000 Subject: [PATCH 497/701] build(deps): bump log4j.version from 2.25.2 to 2.25.3 Bumps `log4j.version` from 2.25.2 to 2.25.3. Updates `org.apache.logging.log4j:log4j-api` from 2.25.2 to 2.25.3 Updates `org.apache.logging.log4j:log4j-core` from 2.25.2 to 2.25.3 Updates `org.apache.logging.log4j:log4j-slf4j2-impl` from 2.25.2 to 2.25.3 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-version: 2.25.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-slf4j2-impl dependency-version: 2.25.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f834970df059..7f292613527b 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 1.1.1 9.4.58.v20250814 - 2.25.2 + 2.25.3 3.0.6 1.19.0 2.0.17 From 570fb9c0fcc5623a8e5701abbc1a1370a7ed6019 Mon Sep 17 00:00:00 2001 From: hsb-app Date: Mon, 26 Jan 2026 13:53:51 +0100 Subject: [PATCH 498/701] #11792 Bug fix link checker stops when url has spaces --- .../java/org/dspace/ctask/general/BasicLinkChecker.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java index 020331842703..476c673ec952 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java @@ -9,12 +9,15 @@ import java.io.IOException; import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; @@ -141,7 +144,8 @@ protected boolean checkURL(String url, StringBuilder results) { protected int getResponseStatus(String url, int redirects) { RequestConfig config = RequestConfig.custom().setRedirectsEnabled(true).build(); try (CloseableHttpClient httpClient = DSpaceHttpClientFactory.getInstance().buildWithRequestConfig(config)) { - CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(url)); + URI uri = new URIBuilder(url).build(); + CloseableHttpResponse httpResponse = httpClient.execute(new HttpGet(uri)); int statusCode = httpResponse.getStatusLine().getStatusCode(); int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0); if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM || @@ -153,6 +157,9 @@ protected int getResponseStatus(String url, int redirects) { } } return statusCode; + } catch (URISyntaxException e) { + log.error("Invalid URL: ", url, e); + return 0; } catch (IOException ioe) { // Must be a bad URL log.debug("Bad link: " + ioe.getMessage()); From 1cb88279efcab123c62150a731ebfe038b2168fd Mon Sep 17 00:00:00 2001 From: zhaw-hsb Date: Mon, 26 Jan 2026 14:38:38 +0100 Subject: [PATCH 499/701] checkstyle removed trailing spaces --- .../main/java/org/dspace/ctask/general/BasicLinkChecker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java index 476c673ec952..3c116df13bd2 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java @@ -158,7 +158,7 @@ protected int getResponseStatus(String url, int redirects) { } return statusCode; } catch (URISyntaxException e) { - log.error("Invalid URL: ", url, e); + log.error("Invalid URL: ", url, e); return 0; } catch (IOException ioe) { // Must be a bad URL From 470985b358569bc203d17135c23fe61297a1c682 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Thu, 16 Oct 2025 17:00:28 +0200 Subject: [PATCH 500/701] [CST-16756] change style for date pickers (cherry picked from commit e57cbc9743a2783575ab0b29ad7f2ac444cb5a01) --- dspace/config/submission-forms.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index 0c5d00e4aa62..032485310a22 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -94,7 +94,7 @@ issued false - + date Please give the date of previous publication or public distribution. You can leave out the day and/or month if they aren't applicable. @@ -108,7 +108,7 @@ false - + onebox Enter the name of the publisher of the previously issued instance of this item. @@ -296,7 +296,7 @@ issued false - + date Please give the date of previous publication or public distribution. You can leave out the day and/or month if they aren't applicable. @@ -310,7 +310,7 @@ false - + onebox Enter the name of the publisher of the previously issued instance of this item. @@ -874,7 +874,7 @@ issued false - + date Please give the date of previous publication or public distribution. You can leave out the day @@ -890,7 +890,7 @@ false - + onebox Enter the name of the publisher of the previously issued instance of this item. From 05a8682049a574e2ee77c01162ebba323edeb93f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:00:05 +0000 Subject: [PATCH 501/701] Bump the fasterxml group with 5 updates Bumps the fasterxml group with 5 updates: | Package | From | To | | --- | --- | --- | | [com.fasterxml:classmate](https://github.com/FasterXML/java-classmate) | `1.7.1` | `1.7.3` | | [com.fasterxml.jackson.core:jackson-annotations](https://github.com/FasterXML/jackson) | `2.20` | `2.21` | | [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core) | `2.20.1` | `2.21.0` | | [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) | `2.20.1` | `2.21.0` | | com.fasterxml.jackson.datatype:jackson-datatype-jsr310 | `2.20.1` | `2.21.0` | Updates `com.fasterxml:classmate` from 1.7.1 to 1.7.3 - [Commits](https://github.com/FasterXML/java-classmate/compare/classmate-1.7.1...classmate-1.7.3) Updates `com.fasterxml.jackson.core:jackson-annotations` from 2.20 to 2.21 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.core:jackson-core` from 2.20.1 to 2.21.0 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.20.1...jackson-core-2.21.0) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.20.1 to 2.21.0 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.20.1 to 2.21.0 Updates `com.fasterxml.jackson.core:jackson-databind` from 2.20.1 to 2.21.0 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.20.1 to 2.21.0 --- updated-dependencies: - dependency-name: com.fasterxml:classmate dependency-version: 1.7.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-annotations dependency-version: '2.21' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency-version: 2.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: fasterxml ... Signed-off-by: dependabot[bot] --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 7f292613527b..c488fb0ef64b 100644 --- a/pom.xml +++ b/pom.xml @@ -31,8 +31,8 @@ 3.11.1 2.42.0 - 2.20.1 - 2.20 + 2.21.0 + 2.21 2.1.1 4.0.4 4.0.6 @@ -1733,7 +1733,7 @@ com.fasterxml classmate - 1.7.1 + 1.7.3 com.fasterxml.jackson.core From 86671f7f22bad03d9bc8bfccfb438cd09306f262 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:01:01 +0000 Subject: [PATCH 502/701] Bump org.checkerframework:checker-qual from 3.52.0 to 3.53.0 Bumps [org.checkerframework:checker-qual](https://github.com/typetools/checker-framework) from 3.52.0 to 3.53.0. - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-3.52.0...checker-framework-3.53.0) --- updated-dependencies: - dependency-name: org.checkerframework:checker-qual dependency-version: 3.53.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7f292613527b..32c605b7075e 100644 --- a/pom.xml +++ b/pom.xml @@ -1350,7 +1350,7 @@ org.checkerframework checker-qual - 3.52.0 + 3.53.0 6.4.10.Final 8.0.3.Final - 42.7.8 + 42.7.9 10.22.0 8.11.4 From 9b278b17d19dcc8aec92d8c20b80ff498aac234c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:02:42 +0000 Subject: [PATCH 506/701] build(deps-dev): bump the test-tools group across 1 directory with 2 updates Bumps the test-tools group with 2 updates in the / directory: com.adobe.testing:s3mock-testcontainers and [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client). Updates `com.adobe.testing:s3mock-testcontainers` from 4.10.0 to 4.11.0 Updates `org.apache.httpcomponents.client5:httpclient5` from 5.5.1 to 5.6 - [Changelog](https://github.com/apache/httpcomponents-client/blob/master/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.5.1...rel/v5.6) --- updated-dependencies: - dependency-name: com.adobe.testing:s3mock-testcontainers dependency-version: 4.11.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: test-tools - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-version: '5.6' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- dspace-server-webapp/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index aaf493d6f2f8..5f255bd8ec3d 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -850,7 +850,7 @@ com.adobe.testing s3mock-testcontainers - 4.10.0 + 4.11.0 test diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 4b050d9f492e..cff5475f6054 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -593,7 +593,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.5.1 + 5.6 test From 1c508a5379d493e61bb5ac09ee2ecdb59d563837 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:12:47 +0000 Subject: [PATCH 507/701] Bump the build-tools group across 1 directory with 6 updates Bumps the build-tools group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [org.sonatype.central:central-publishing-maven-plugin](https://github.com/sonatype/central-publishing-maven-plugin) | `0.9.0` | `0.10.0` | | [org.apache.maven.plugins:maven-release-plugin](https://github.com/apache/maven-release) | `3.2.0` | `3.3.1` | | [org.codehaus.mojo:xml-maven-plugin](https://github.com/mojohaus/xml-maven-plugin) | `1.2.0` | `1.2.1` | | [org.codehaus.mojo:license-maven-plugin](https://github.com/mojohaus/license-maven-plugin) | `2.7.0` | `2.7.1` | | [org.codehaus.mojo:buildnumber-maven-plugin](https://github.com/mojohaus/buildnumber-maven-plugin) | `3.2.1` | `3.3.0` | | [org.codehaus.mojo:properties-maven-plugin](https://github.com/mojohaus/properties-maven-plugin) | `1.2.1` | `1.3.0` | Updates `org.sonatype.central:central-publishing-maven-plugin` from 0.9.0 to 0.10.0 - [Commits](https://github.com/sonatype/central-publishing-maven-plugin/commits) Updates `org.apache.maven.plugins:maven-release-plugin` from 3.2.0 to 3.3.1 - [Release notes](https://github.com/apache/maven-release/releases) - [Commits](https://github.com/apache/maven-release/compare/maven-release-3.2.0...maven-release-3.3.1) Updates `org.codehaus.mojo:xml-maven-plugin` from 1.2.0 to 1.2.1 - [Release notes](https://github.com/mojohaus/xml-maven-plugin/releases) - [Commits](https://github.com/mojohaus/xml-maven-plugin/compare/1.2.0...xml-maven-plugin-1.2.1) Updates `org.codehaus.mojo:license-maven-plugin` from 2.7.0 to 2.7.1 - [Release notes](https://github.com/mojohaus/license-maven-plugin/releases) - [Commits](https://github.com/mojohaus/license-maven-plugin/compare/2.7.0...2.7.1) Updates `org.codehaus.mojo:buildnumber-maven-plugin` from 3.2.1 to 3.3.0 - [Release notes](https://github.com/mojohaus/buildnumber-maven-plugin/releases) - [Commits](https://github.com/mojohaus/buildnumber-maven-plugin/compare/3.2.1...buildnumber-maven-plugin-3.3.0) Updates `org.codehaus.mojo:properties-maven-plugin` from 1.2.1 to 1.3.0 - [Release notes](https://github.com/mojohaus/properties-maven-plugin/releases) - [Commits](https://github.com/mojohaus/properties-maven-plugin/compare/1.2.1...properties-maven-plugin-1.3.0) --- updated-dependencies: - dependency-name: org.sonatype.central:central-publishing-maven-plugin dependency-version: 0.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-release-plugin dependency-version: 3.3.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.codehaus.mojo:xml-maven-plugin dependency-version: 1.2.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.codehaus.mojo:license-maven-plugin dependency-version: 2.7.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.codehaus.mojo:buildnumber-maven-plugin dependency-version: 3.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.codehaus.mojo:properties-maven-plugin dependency-version: 1.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 2 +- dspace-server-webapp/pom.xml | 2 +- pom.xml | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index aaf493d6f2f8..78fa8f887355 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -102,7 +102,7 @@ org.codehaus.mojo buildnumber-maven-plugin - 3.2.1 + 3.3.0 UNKNOWN_REVISION diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 4b050d9f492e..db886f66a52e 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -31,7 +31,7 @@ org.codehaus.mojo properties-maven-plugin - 1.2.1 + 1.3.0 initialize diff --git a/pom.xml b/pom.xml index 7f292613527b..c63363e05fa8 100644 --- a/pom.xml +++ b/pom.xml @@ -367,7 +367,7 @@ org.sonatype.central central-publishing-maven-plugin - 0.9.0 + 0.10.0 @@ -419,7 +419,7 @@ org.apache.maven.plugins maven-release-plugin - 3.2.0 + 3.3.1 @@ -487,7 +487,7 @@ org.codehaus.mojo xml-maven-plugin - 1.2.0 + 1.2.1 validate-ALL-xml-and-xsl @@ -695,7 +695,7 @@ org.codehaus.mojo license-maven-plugin - 2.7.0 + 2.7.1 false From 2a9108f7482af4582b01f7745a4fa98a57c36109 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:13:51 +0000 Subject: [PATCH 508/701] build(deps): bump the amazon-s3 group with 2 updates Bumps the amazon-s3 group with 2 updates: software.amazon.awssdk:s3 and [software.amazon.awssdk.crt:aws-crt](https://github.com/awslabs/aws-crt-java). Updates `software.amazon.awssdk:s3` from 2.38.8 to 2.39.6 Updates `software.amazon.awssdk.crt:aws-crt` from 0.39.4 to 0.40.1 - [Release notes](https://github.com/awslabs/aws-crt-java/releases) - [Commits](https://github.com/awslabs/aws-crt-java/compare/v0.39.4...v0.40.1) --- updated-dependencies: - dependency-name: software.amazon.awssdk:s3 dependency-version: 2.39.6 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: amazon-s3 - dependency-name: software.amazon.awssdk.crt:aws-crt dependency-version: 0.40.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: amazon-s3 ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index aaf493d6f2f8..09cc2fc2f211 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -718,7 +718,7 @@ software.amazon.awssdk s3 - 2.38.8 + 2.41.14 software.amazon.awssdk @@ -734,7 +734,7 @@ software.amazon.awssdk.crt aws-crt - 0.39.4 + 0.42.2 @@ -1532,12 +1532,12 @@ org.apache.commons commons-pool2 - 2.12.1 + 2.13.1 org.apache.commons commons-text - 1.14.0 + 1.15.0 commons-validator From 9d94f49885a9f41e1871a2c6a2d39f70fc1ee448 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:33:38 +0000 Subject: [PATCH 510/701] build(deps): bump the spring group with 24 updates Bumps the spring group with 24 updates: | Package | From | To | | --- | --- | --- | | [org.springframework:spring-orm](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-core](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-beans](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-aop](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-context](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-context-support](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-tx](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-jdbc](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-web](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-expression](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework:spring-test](https://github.com/spring-projects/spring-framework) | `6.2.14` | `6.2.15` | | [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-tomcat](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-maven-plugin](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-cache](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-thymeleaf](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-web](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-data-rest](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-security](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-aop](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-actuator](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | | [org.springframework.boot:spring-boot-starter-log4j2](https://github.com/spring-projects/spring-boot) | `3.5.8` | `3.5.9` | Updates `org.springframework:spring-orm` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-core` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-beans` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-aop` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-context` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-context-support` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-tx` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-jdbc` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-web` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-webmvc` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-expression` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-test` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-core` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-beans` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-aop` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-context` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-context-support` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-tx` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-jdbc` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-web` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-webmvc` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-expression` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework:spring-test` from 6.2.14 to 6.2.15 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.14...v6.2.15) Updates `org.springframework.boot:spring-boot-starter-test` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.8 to 3.5.9 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.8...v3.5.9) --- updated-dependencies: - dependency-name: org.springframework:spring-orm dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7f292613527b..b3a00e5950ab 100644 --- a/pom.xml +++ b/pom.xml @@ -19,8 +19,8 @@ 17 - 6.2.14 - 3.5.8 + 6.2.15 + 3.5.10 6.5.7 6.4.10.Final 8.0.3.Final From baccd0aea902392d89314cd9396278d2e466ccd1 Mon Sep 17 00:00:00 2001 From: MMilosz Date: Thu, 29 Jan 2026 23:08:15 +0100 Subject: [PATCH 511/701] chore: remove commons-configuration2 workaround #11812 DSpace previously shipped a local copy of Apache Commons Configuration's ConfigurationPropertySource as a workaround for CONFIGURATION-846 issue (see refs), which affected commons-configuration2 versions 2.10.0 and 2.10.1. That issue was fixed upstream in 2.11.0 and later. Since DSpace now depends on commons-configuration2 v2.13.0, the DSpaceConfigurationPropertySource workaround class is no longer necessary. Historical context (commits with 'DURACOM-267'): - 7fc74bdc56, 4fcf995b69: ensure custom PropertySource used in kernel Spring context - b9e6af6758, 0422b8786f: improve handling of empty configuration values - 4b5248fe15, f2d4ffc49c: update workaround per community feedback Refs: - https://issues.apache.org/jira/browse/CONFIGURATION-846 - https://github.com/DSpace/DSpace/pull/9605 (cherry picked from commit b51a5d6bb42216aa27088ece371324f48df9e4a1) --- .../util/DSpaceConfigurationInitializer.java | 6 +- .../utils/DSpaceConfigurationInitializer.java | 6 +- ...aceConfigurationPlaceholderConfigurer.java | 5 +- .../DSpaceConfigurationPropertySource.java | 64 ------------------- pom.xml | 2 - 5 files changed, 9 insertions(+), 74 deletions(-) delete mode 100644 dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java diff --git a/dspace-api/src/test/java/org/dspace/util/DSpaceConfigurationInitializer.java b/dspace-api/src/test/java/org/dspace/util/DSpaceConfigurationInitializer.java index e5a8adb2fdf7..e2e0355f123a 100644 --- a/dspace-api/src/test/java/org/dspace/util/DSpaceConfigurationInitializer.java +++ b/dspace-api/src/test/java/org/dspace/util/DSpaceConfigurationInitializer.java @@ -8,7 +8,7 @@ package org.dspace.util; import org.apache.commons.configuration2.Configuration; -import org.dspace.servicemanager.config.DSpaceConfigurationPropertySource; +import org.apache.commons.configuration2.spring.ConfigurationPropertySource; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.context.ApplicationContextInitializer; @@ -38,8 +38,8 @@ public void initialize(final ConfigurableApplicationContext applicationContext) Configuration configuration = configurationService.getConfiguration(); // Create an Apache Commons Configuration Property Source from our configuration - DSpaceConfigurationPropertySource apacheCommonsConfigPropertySource = - new DSpaceConfigurationPropertySource(configuration.getClass().getName(), configuration); + ConfigurationPropertySource apacheCommonsConfigPropertySource = + new ConfigurationPropertySource(configuration.getClass().getName(), configuration); // Prepend it to the Environment's list of PropertySources // NOTE: This is added *first* in the list so that settings in DSpace's diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java index c04ac976e0ac..d63dc8c55a11 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java @@ -8,7 +8,7 @@ package org.dspace.app.rest.utils; import org.apache.commons.configuration2.Configuration; -import org.dspace.servicemanager.config.DSpaceConfigurationPropertySource; +import org.apache.commons.configuration2.spring.ConfigurationPropertySource; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.context.ApplicationContextInitializer; @@ -34,8 +34,8 @@ public void initialize(final ConfigurableApplicationContext applicationContext) Configuration configuration = configurationService.getConfiguration(); // Create an Apache Commons Configuration Property Source from our configuration - DSpaceConfigurationPropertySource apacheCommonsConfigPropertySource = - new DSpaceConfigurationPropertySource(configuration.getClass().getName(), configuration); + ConfigurationPropertySource apacheCommonsConfigPropertySource = + new ConfigurationPropertySource(configuration.getClass().getName(), configuration); // Prepend it to the Environment's list of PropertySources // NOTE: This is added *first* in the list so that settings in DSpace's ConfigurationService *override* diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPlaceholderConfigurer.java b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPlaceholderConfigurer.java index caa715e21bfb..b85450dcd039 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPlaceholderConfigurer.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPlaceholderConfigurer.java @@ -8,6 +8,7 @@ package org.dspace.servicemanager.config; import org.apache.commons.configuration2.Configuration; +import org.apache.commons.configuration2.spring.ConfigurationPropertySource; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.env.MutablePropertySources; @@ -26,8 +27,8 @@ public class DSpaceConfigurationPlaceholderConfigurer extends PropertySourcesPlaceholderConfigurer { public DSpaceConfigurationPlaceholderConfigurer(Configuration configuration) { - DSpaceConfigurationPropertySource apacheCommonsConfigPropertySource = - new DSpaceConfigurationPropertySource(configuration.getClass().getName(), configuration); + ConfigurationPropertySource apacheCommonsConfigPropertySource = + new ConfigurationPropertySource(configuration.getClass().getName(), configuration); MutablePropertySources propertySources = new MutablePropertySources(); propertySources.addLast(apacheCommonsConfigPropertySource); setPropertySources(propertySources); diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java deleted file mode 100644 index d3394399301f..000000000000 --- a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.dspace.servicemanager.config; - -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.configuration2.Configuration; -import org.apache.commons.lang3.ArrayUtils; -import org.springframework.core.env.EnumerablePropertySource; - -/** - * Allow use of Apache Commons Configuration Objects as Spring PropertySources. - * This class is a temporary copy of the ConfigurationPropertySource class in the Apache Commons Configuration - * project needed until to fix the issue https://issues.apache.org/jira/browse/CONFIGURATION-846 - */ -public class DSpaceConfigurationPropertySource extends EnumerablePropertySource { - - protected DSpaceConfigurationPropertySource(final String name) { - super(name); - } - - public DSpaceConfigurationPropertySource(final String name, final Configuration source) { - super(name, source); - } - - @Override - public Object getProperty(final String name) { - if (source.getProperty(name) != null) { - final String[] propValue = source.getStringArray(name); - if (propValue == null || propValue.length == 0) { - return ""; - } else if (propValue.length == 1) { - return propValue[0]; - } else { - return propValue; - } - } else { - return null; - } - } - - @Override - public String[] getPropertyNames() { - final List keys = new ArrayList<>(); - source.getKeys().forEachRemaining(keys::add); - return keys.toArray(ArrayUtils.EMPTY_STRING_ARRAY); - } -} diff --git a/pom.xml b/pom.xml index 7f292613527b..69abfb10b287 100644 --- a/pom.xml +++ b/pom.xml @@ -448,8 +448,6 @@ - - **/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationPropertySource.java **/src/test/resources/** **/src/test/data/** **/src/main/license/** From 9c149c85b3406930e54fbbb9c439fe4ef915c75d Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 21 Nov 2025 12:56:26 +0100 Subject: [PATCH 512/701] feat(ror): Adds client-id in requests if provided by config ref: DSC-2444 (cherry picked from commit 6a368ca6a03a37090f919e587e2c67cd7ac5739b) --- .../RorImportMetadataSourceServiceImpl.java | 23 +++++++++++++++---- dspace/config/modules/external-providers.cfg | 3 +++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java index 2cc6c66c7ab3..4ee3404f7b9b 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java @@ -7,6 +7,8 @@ */ package org.dspace.importer.external.ror.service; +import static org.dspace.importer.external.liveimportclient.service.LiveImportClientImpl.HEADER_PARAMETERS; + import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; @@ -31,6 +33,7 @@ import org.dspace.importer.external.liveimportclient.service.LiveImportClient; import org.dspace.importer.external.service.AbstractImportMetadataSourceService; import org.dspace.importer.external.service.components.QuerySource; +import org.dspace.services.factory.DSpaceServicesFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -43,6 +46,8 @@ public class RorImportMetadataSourceServiceImpl extends AbstractImportMetadataSo private final static Logger log = LogManager.getLogger(); protected static final String ROR_IDENTIFIER_PREFIX = "https://ror.org/"; + protected static final String ROR_CLIENT_ID_HEADER = "Client-Id"; + protected static final String ROR_CLIENT_ID_PROP = "ror.client-id"; private String url; @@ -160,7 +165,7 @@ public List call() throws Exception { * ROR query. This Callable uses as query value to ROR the string queryString * passed to constructor. If the object will be construct through {@code Query} * instance, the value of the Query's map with the key "query" will be used. - * + * * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) */ private class CountByQueryCallable implements Callable { @@ -189,7 +194,7 @@ public Integer call() throws Exception { */ public Integer count(String query) { try { - Map> params = new HashMap>(); + Map> params = getBaseParams(); URIBuilder uriBuilder = new URIBuilder(this.url); uriBuilder.addParameter("query", query); @@ -213,7 +218,7 @@ private List searchById(String id) { id = StringUtils.removeStart(id, ROR_IDENTIFIER_PREFIX); try { - Map> params = new HashMap>(); + Map> params = getBaseParams(); URIBuilder uriBuilder = new URIBuilder(this.url + "/" + id); @@ -234,7 +239,7 @@ private List searchById(String id) { private List search(String query) { List importResults = new ArrayList<>(); try { - Map> params = new HashMap>(); + Map> params = getBaseParams(); URIBuilder uriBuilder = new URIBuilder(this.url); uriBuilder.addParameter("query", query); @@ -261,6 +266,16 @@ private List search(String query) { return importResults; } + protected Map> getBaseParams() { + Map> params = new HashMap<>(); + String rorClientId = + DSpaceServicesFactory.getInstance().getConfigurationService().getProperty(ROR_CLIENT_ID_PROP); + if (StringUtils.isNotEmpty(rorClientId)) { + params.put(HEADER_PARAMETERS, Map.of(ROR_CLIENT_ID_HEADER, rorClientId)); + } + return params; + } + private JsonNode convertStringJsonToJsonNode(String json) { try { return new ObjectMapper().readTree(json); diff --git a/dspace/config/modules/external-providers.cfg b/dspace/config/modules/external-providers.cfg index 254207febdd6..cbb2b4ea0cbf 100644 --- a/dspace/config/modules/external-providers.cfg +++ b/dspace/config/modules/external-providers.cfg @@ -99,4 +99,7 @@ datacite.timeout = 180000 #---------------------------------------------------------------# ror.orgunit-import.api-url = https://api.ror.org/v2/organizations +# This client-id must be set inside local.cfg you can find details on how to generate it here: +# https://ror.readme.io/docs/client-id#how-to-register-a-client-id +# ror.client-id = ################################################################# From 98ad29a041203a57c11e11f9b70dbeb61d52a338 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Tue, 3 Feb 2026 21:25:08 +0100 Subject: [PATCH 513/701] Move same-collection check to inheritDefaultPolicies move method (cherry picked from commit cfff8040b3f0fcf079f87fef51451b12107c80b6) --- .../org/dspace/content/ItemServiceImpl.java | 10 ++++----- .../java/org/dspace/content/ItemTest.java | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 99dc9459d84f..998cde590e78 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -1142,11 +1142,6 @@ public void adjustItemPolicies(Context context, Item item, Collection collection public void move(Context context, Item item, Collection from, Collection to) throws SQLException, AuthorizeException, IOException { - // If the two collections are the same, do nothing. - if (from.equals(to)) { - return; - } - // Use the normal move method, and default to not inherit permissions this.move(context, item, from, to, false); } @@ -1161,6 +1156,11 @@ public void move(Context context, Item item, Collection from, Collection to, boo authorizeService.authorizeAction(context, item, Constants.WRITE); } + // If the two collections are the same, do nothing. + if (from.equals(to)) { + return; + } + // Move the Item from one Collection to the other collectionService.addItem(context, to, item); collectionService.removeItem(context, from, item); diff --git a/dspace-api/src/test/java/org/dspace/content/ItemTest.java b/dspace-api/src/test/java/org/dspace/content/ItemTest.java index 00dbf2994d98..1f520926b982 100644 --- a/dspace-api/src/test/java/org/dspace/content/ItemTest.java +++ b/dspace-api/src/test/java/org/dspace/content/ItemTest.java @@ -1604,6 +1604,27 @@ public void testMoveSameCollection() throws Exception { verify(itemServiceSpy, times(0)).delete(context, it); } + /** + * Test of move with inherit default policies method, of class Item, where both Collections are the same. + */ + @Test + public void testMoveSameCollectionWithInheritDefaultPolicies() throws Exception { + context.turnOffAuthorisationSystem(); + while (it.getCollections().size() > 1) { + it.removeCollection(it.getCollections().get(0)); + } + + Collection collection = it.getCollections().get(0); + it.setOwningCollection(collection); + ItemService itemServiceSpy = spy(itemService); + + itemService.move(context, it, collection, collection, true); + context.restoreAuthSystemState(); + assertThat("testMoveSameCollection 0", it.getOwningCollection(), notNullValue()); + assertThat("testMoveSameCollection 1", it.getOwningCollection(), equalTo(collection)); + verify(itemServiceSpy, times(0)).delete(context, it); + } + /** * Test of hasUploadedFiles method, of class Item. */ From 9c366d92a9a7f3d35a748ecce2102eea6ad75fb1 Mon Sep 17 00:00:00 2001 From: Jozef Misutka <332350+vidiecan@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:03:02 +0100 Subject: [PATCH 514/701] Update link for Registered Service Providers (cherry picked from commit 8c780d1ebca37d471781ffe42a22d5a637ad7be8) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d93abe49948..53e7e8cc3461 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Additional support options are at https://wiki.lyrasis.org/display/DSPACE/Suppor DSpace also has an active service provider network. If you'd rather hire a service provider to install, upgrade, customize, or host DSpace, then we recommend getting in touch with one of our -[Registered Service Providers](http://www.dspace.org/service-providers). +[Registered Service Providers](https://dspace.org/registered-service-providers/). ## Issue Tracker From 6507f15663bb3cf3f1e68f3222602500cdc9d325 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Thu, 11 Dec 2025 13:40:42 +0100 Subject: [PATCH 515/701] added test for getVetsion, fix null pointer exception (cherry picked from commit 8a18ca6a79bd2433734088d00ff03170ba38ee59) --- .../versioning/VersionHistoryServiceImpl.java | 3 --- .../org/dspace/content/VersioningTest.java | 21 +++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java index 96c39ac3a8e8..493861df1c60 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/versioning/VersionHistoryServiceImpl.java @@ -109,9 +109,6 @@ public Version getVersion(Context context, VersionHistory versionHistory, Item i throws SQLException { Version v = versioningService.getVersion(context, item); if (v != null) { - ; - } - { if (versionHistory.equals(v.getVersionHistory())) { return v; } diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java index c23fe7831967..3723bae2e32f 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java @@ -170,4 +170,25 @@ public void testVersionDelete() throws Exception { assertThat("Test_version_handle_delete", handleService.resolveToObject(context, handle), nullValue()); context.restoreAuthSystemState(); } + + @Test + public void testGetVersionWithNullPointerException() throws Exception { + context.turnOffAuthorisationSystem(); + + // Create item without version + Community community = communityService.create(null, context); + Collection col = collectionService.create(context, community); + WorkspaceItem is = workspaceItemService.create(context, col, false); + Item itemWithoutVersion = installItemService.installItem(context, is); + + VersionHistory versionHistory = versionHistoryService.findByItem(context, originalItem); + + try { + Version result = versionHistoryService.getVersion(context, versionHistory, itemWithoutVersion); + assertThat("getVersion should return null for item without version", result, nullValue()); + } catch (NullPointerException npe) { + fail("NullPointerException should not be thrown. Method should return null gracefully: " + npe.getMessage()); + } + context.restoreAuthSystemState(); + } } From e67ba9626ffa68bff7bd0a2b69a802b5a00a54a0 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Thu, 11 Dec 2025 13:51:33 +0100 Subject: [PATCH 516/701] fix info message (cherry picked from commit 14d4f4fe9beca9faeb43671e899e24efeb0d4710) --- dspace-api/src/test/java/org/dspace/content/VersioningTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java index 3723bae2e32f..fb971dab79fe 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java @@ -187,7 +187,7 @@ public void testGetVersionWithNullPointerException() throws Exception { Version result = versionHistoryService.getVersion(context, versionHistory, itemWithoutVersion); assertThat("getVersion should return null for item without version", result, nullValue()); } catch (NullPointerException npe) { - fail("NullPointerException should not be thrown. Method should return null gracefully: " + npe.getMessage()); + fail("NullPointerException should not be thrown. Method should return null: " + npe.getMessage()); } context.restoreAuthSystemState(); } From 56d0078667fc172a86b2803a0a4a05c788b4f41b Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Thu, 11 Dec 2025 14:11:27 +0100 Subject: [PATCH 517/701] added restoreAuthSystemState to finally block (cherry picked from commit f22766a500553bfc87efe3ec2693a2421d4a6a3e) --- .../src/test/java/org/dspace/content/VersioningTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java index fb971dab79fe..5045412969a4 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java @@ -188,7 +188,8 @@ public void testGetVersionWithNullPointerException() throws Exception { assertThat("getVersion should return null for item without version", result, nullValue()); } catch (NullPointerException npe) { fail("NullPointerException should not be thrown. Method should return null: " + npe.getMessage()); + } finally { + context.restoreAuthSystemState(); } - context.restoreAuthSystemState(); } } From 2de214618f38bdb73fc649099b7ff83a2c5650b5 Mon Sep 17 00:00:00 2001 From: Paurikova2 Date: Thu, 11 Dec 2025 15:08:56 +0100 Subject: [PATCH 518/701] removed empty lines because of the trailing whitespace (cherry picked from commit 396e8437f8f8b6347729d29a5b0a5d711f20f565) --- .../src/test/java/org/dspace/content/VersioningTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java index 5045412969a4..a0a8b82ec150 100644 --- a/dspace-api/src/test/java/org/dspace/content/VersioningTest.java +++ b/dspace-api/src/test/java/org/dspace/content/VersioningTest.java @@ -174,15 +174,12 @@ public void testVersionDelete() throws Exception { @Test public void testGetVersionWithNullPointerException() throws Exception { context.turnOffAuthorisationSystem(); - // Create item without version Community community = communityService.create(null, context); Collection col = collectionService.create(context, community); WorkspaceItem is = workspaceItemService.create(context, col, false); Item itemWithoutVersion = installItemService.installItem(context, is); - VersionHistory versionHistory = versionHistoryService.findByItem(context, originalItem); - try { Version result = versionHistoryService.getVersion(context, versionHistory, itemWithoutVersion); assertThat("getVersion should return null for item without version", result, nullValue()); From 3fdca9c9ce2f17af62fa59459c5f5949ba172e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Wed, 30 Jul 2025 11:46:04 +0100 Subject: [PATCH 519/701] removing unused collection convertion (cherry picked from commit a5c2cff6294d99c17ad3b68ce059202dfaeb6b92) --- .../converter/SubmissionDefinitionConverter.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java index 8e4fd247874c..209be7728a0f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java @@ -75,21 +75,6 @@ public SubmissionDefinitionRest convert(SubmissionConfig obj, Projection project e); } } - - HttpServletRequest request = requestService.getCurrentRequest().getHttpServletRequest(); - Context context = null; - try { - context = ContextUtil.obtainContext(request); - List collections = panelConverter.getSubmissionConfigService() - .getCollectionsBySubmissionConfig(context, - obj.getSubmissionName()); - DSpaceConverter cc = converter.getConverter(Collection.class); - List collectionsRest = collections.stream().map((collection) -> - cc.convert(collection, projection)).collect(Collectors.toList()); - sd.setCollections(collectionsRest); - } catch (SQLException | IllegalStateException | SubmissionConfigReaderException e) { - log.error(e.getMessage(), e); - } sd.setPanels(panels); return sd; } From 38d3e60fba891ab7372d8d904760d383e65059df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Wed, 30 Jul 2025 15:11:21 +0100 Subject: [PATCH 520/701] adjust tests and stylecheck fixes (cherry picked from commit 15f1ab3e27b8e6ab1b4225bcdc98c39f74664df1) --- .../app/rest/converter/SubmissionDefinitionConverter.java | 8 -------- .../app/rest/SubmissionDefinitionsControllerIT.java | 2 +- .../app/rest/matcher/SubmissionDefinitionsMatcher.java | 2 +- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java index 209be7728a0f..55c385b27155 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionDefinitionConverter.java @@ -7,24 +7,16 @@ */ package org.dspace.app.rest.converter; -import java.sql.SQLException; import java.util.LinkedList; import java.util.List; -import java.util.stream.Collectors; -import jakarta.servlet.http.HttpServletRequest; import org.apache.logging.log4j.Logger; -import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.SubmissionDefinitionRest; import org.dspace.app.rest.model.SubmissionSectionRest; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.submit.DataProcessingStep; -import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.util.SubmissionConfig; -import org.dspace.app.util.SubmissionConfigReaderException; import org.dspace.app.util.SubmissionStepConfig; -import org.dspace.content.Collection; -import org.dspace.core.Context; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index bcea11cbb5a4..77c6df2554e9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -191,7 +191,7 @@ public void findCollections() throws Exception { //Match only that a section exists with a submission configuration behind getClient(token).perform(get("/api/config/submissiondefinitions/traditional/collections") .param("projection", "full")) - .andExpect(status().isOk()) + .andExpect(status().isNoContent()) .andExpect(jsonPath("$.page.totalElements", is(0))); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionDefinitionsMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionDefinitionsMatcher.java index 398097db6fba..14d8e5143815 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionDefinitionsMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionDefinitionsMatcher.java @@ -34,7 +34,7 @@ public static Matcher matchSubmissionDefinition(boolean isDefault, Strin */ public static Matcher matchFullEmbeds() { return matchEmbeds( - "collections[]", + "collections", "sections" ); } From 48df63456a7fd03737698f881aa7e5a70c05d2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Wed, 30 Jul 2025 16:00:59 +0100 Subject: [PATCH 521/701] remove page total elements comparison (cherry picked from commit 6657be51af5ba7d90bf6cdf04f3baad99c7478a4) --- .../org/dspace/app/rest/SubmissionDefinitionsControllerIT.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index 77c6df2554e9..b8156396d309 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -191,8 +191,7 @@ public void findCollections() throws Exception { //Match only that a section exists with a submission configuration behind getClient(token).perform(get("/api/config/submissiondefinitions/traditional/collections") .param("projection", "full")) - .andExpect(status().isNoContent()) - .andExpect(jsonPath("$.page.totalElements", is(0))); + .andExpect(status().isNoContent()); } @Test From d9c6694e15f3a0d7bce6cf84183aec3f0dcec157 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 11 Feb 2026 21:47:25 +0100 Subject: [PATCH 522/701] Type date browse startsWith functionality - input date as start date, not date filter (#10056) * 121661: [Issue#10055] startsWith on date browse should return items starting from that date, not with that exact year https://github.com/DSpace/DSpace/issues/10055 * 121661: [Issue#10055, PR#10056] Use upperBound year when sorting by startsWith date descending, so a year implies => up to end of year, month implied => up to end of month --- .../java/org/dspace/browse/BrowseDAO.java | 2 + .../java/org/dspace/browse/BrowseEngine.java | 32 +++++-- .../java/org/dspace/browse/SolrBrowseDAO.java | 31 +++++++ .../app/rest/BrowsesResourceControllerIT.java | 85 ++++++++++++++++--- 4 files changed, 131 insertions(+), 19 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java index 03130e39e78b..26983303a152 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseDAO.java @@ -396,4 +396,6 @@ public interface BrowseDAO { public void setStartsWith(String startsWith); public String getStartsWith(); + + public void setDateStartsWith(String dateStartsWith); } diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java index be7a34086a46..f7f5f2ff7df7 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseEngine.java @@ -203,12 +203,8 @@ private BrowseInfo browseByItem(BrowserScope bs) // get the table name that we are going to be getting our data from dao.setTable(browseIndex.getTableName()); - if (scope.getBrowseIndex() != null && OrderFormat.TITLE.equals(scope.getBrowseIndex().getDataType())) { - // For browsing by title, apply the same normalization applied to indexed titles - dao.setStartsWith(normalizeJumpToValue(scope.getStartsWith())); - } else { - dao.setStartsWith(StringUtils.lowerCase(scope.getStartsWith())); - } + // Set startsWith or dateStartsWith params on SolrBrowseDAO + addStartsWithParams(bs); // tell the browse query whether we are ascending or descending on the value dao.setAscending(scope.isAscending()); @@ -367,6 +363,30 @@ private BrowseInfo browseByItem(BrowserScope bs) } } + private void addStartsWithParams(BrowserScope bs) throws BrowseException { + if (StringUtils.isNotBlank(scope.getStartsWith())) { + boolean isDateBrowse = bs.getSortOption().getType().equals("date"); + if (!isDateBrowse) { + if (scope.getBrowseIndex() != null + && OrderFormat.TITLE.equals(scope.getBrowseIndex().getDataType())) { + // For browsing by title, apply the same normalization applied to indexed titles + dao.setStartsWith(normalizeJumpToValue(scope.getStartsWith())); + } else { + dao.setStartsWith(StringUtils.lowerCase(scope.getStartsWith())); + } + // clear the old date starts with + dao.setDateStartsWith(null); + } else { + // For "date" sort browses ({@code webui.itemlist.sort-option.*} config): + // sets a date specific filter where the startsWith query is the start date, + // eg `fq=bi_sort_*_sort:+["1940-02" TO + ]` + dao.setDateStartsWith(scope.getStartsWith().trim()); + // clear the old non date starts with + dao.setStartsWith(null); + } + } + } + /** * Browse the archive by single values (such as the name of an author). This * produces a BrowseInfo object that contains Strings as the results of diff --git a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java index a0a7725fa13a..32841e6c4b34 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -11,6 +11,7 @@ import static org.dspace.discovery.SearchUtils.RESOURCE_TYPE_FIELD; import java.io.Serializable; +import java.time.YearMonth; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -99,6 +100,8 @@ public int compare(Object o1, Object o2) { private String startsWith = null; + private String dateStartsWith = null; + /** * field to look for value in */ @@ -221,10 +224,33 @@ private DiscoverResult getSolrResponse() throws BrowseException { } else if (valuePartial) { query.addFilterQueries("{!field f=" + facetField + "_partial}" + value); } + if (StringUtils.isNotBlank(startsWith) && orderField != null) { query.addFilterQueries( "bi_" + orderField + "_sort:" + ClientUtils.escapeQueryChars(startsWith) + "*"); } + if (StringUtils.isNotBlank(dateStartsWith)) { + if (!ascending) { + String raw = dateStartsWith.trim(); + String upperBound; + if (raw.length() == 4) { // YYYY + upperBound = raw + "-12-31"; + } else if (raw.length() == 7) { // YYYY-MM + YearMonth ym = YearMonth.parse(raw); + upperBound = ym.atEndOfMonth().toString(); + } else { // YYYY-MM-DD + upperBound = raw; + } + query.addFilterQueries("bi_" + orderField + "_sort" + ": [* TO \"" + upperBound + "\"]"); + } else { + query.addFilterQueries("bi_" + orderField + "_sort" + ": [\"" + dateStartsWith + "\" TO *]"); + } + } + if (StringUtils.isNotBlank(startsWith) && StringUtils.isNotBlank(dateStartsWith)) { + log.warn(String.format("dateStartsWith %s and startsWith %s both given, only one should " + + "be given since different type of sort filterquery applied depending on which is not blank", + dateStartsWith, startsWith)); + } // filter on item to be sure to don't include any other object // indexed in the Discovery Search core query.addFilterQueries("search.resourcetype:" + IndexableItem.TYPE); @@ -466,6 +492,11 @@ public int getLimit() { return limit; } + @Override + public void setDateStartsWith(String dateStartsWith) { + this.dateStartsWith = dateStartsWith; + } + /* * (non-Javadoc) * diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java index 1ed1e23260f9..eaef4bd23f9a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java @@ -1885,8 +1885,8 @@ public void testBrowseByItemsStartsWith() throws Exception { // ---- BROWSES BY ITEM ---- //** WHEN ** //An anonymous user browses the items in the Browse by date issued endpoint - //with startsWith set to 199 - getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=199") + //with startsWith set to 1990 + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990") .param("size", "2")) //** THEN ** @@ -1895,8 +1895,8 @@ public void testBrowseByItemsStartsWith() throws Exception { //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //We expect the totalElements to be the 2 items present in the repository - .andExpect(jsonPath("$.page.totalElements", is(2))) + //We expect the totalElements to be the 5 items from 1990 til now + .andExpect(jsonPath("$.page.totalElements", is(5))) //We expect to jump to page 1 of the index .andExpect(jsonPath("$.page.number", is(0))) .andExpect(jsonPath("$.page.size", is(2))) @@ -2057,8 +2057,8 @@ public void testBrowseByStartsWithAndPage() throws Exception { //** WHEN ** //An anonymous user browses the items in the Browse by date issued endpoint - //with startsWith set to 199 and Page to 1 - getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=199") + //with startsWith set to 1990 and Page to 1 + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990") .param("size", "1").param("page", "1")) //** THEN ** @@ -2067,17 +2067,76 @@ public void testBrowseByStartsWithAndPage() throws Exception { //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //We expect the totalElements to be the 2 items present in the repository - .andExpect(jsonPath("$.page.totalElements", is(2))) + //We expect the totalElements to be the 5 items present in the repository from 1990 until now + .andExpect(jsonPath("$.page.totalElements", is(5))) //We expect to jump to page 1 of the index .andExpect(jsonPath("$.page.number", is(1))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$._links.self.href", containsString("startsWith=199"))) + .andExpect(jsonPath("$._links.self.href", containsString("startsWith=1990"))) - //Verify that the index jumps to the "Java" item. - .andExpect(jsonPath("$._embedded.items", - contains( - ItemMatcher.matchItemWithTitleAndDateIssued(item3, "Java", "1995-05-23") + //Verify that the returned item is 2nd (page 0 first item, page 1 second item) item from 1990 + // Items: Alan Turing - 1912; Blade Runner - 1982-06-25 || Python - 1990; + // Java - 1995-05-23; Zeta Reticuli - 2018-01-01; Moon - 2018-01-02; T-800 - 2029 + // 2nd since 1990: Java + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item3, + "Java", "1995-05-23") + ))); + + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990") + .param("size", "2").param("page", "1")) + //Verify that the returned item is 3rd&4th item from 1990 + // Items: Alan Turing - 1912; Blade Runner - 1982-06-25 || Python - 1990; + // Java - 1995-05-23; Zeta Reticuli - 2018-01-01; Moon - 2018-01-02; T-800 - 2029 + // => Zeta Reticuli & Moon + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item7, + "Zeta Reticuli", "2018-01-01"), + ItemMatcher.matchItemWithTitleAndDateIssued(item4, + "Moon", "2018-01-02") + ))); + + // Sort descending + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990&sort=default,DESC") + .param("size", "2").param("page", "0")) + //Verify that the returned items are from 1990 and below dates + // Items: Alan Turing - 1912; Blade Runner - 1982-06-25 || Python - 1990; + // Java - 1995-05-23; Zeta Reticuli - 2018-01-01; Moon - 2018-01-02; T-800 - 2029 + // => Python & Blade Runner + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item5, + "Python", "1990"), + ItemMatcher.matchItemWithTitleAndDateIssued(item2, + "Blade Runner", "1982-06-25") + ))); + + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990&sort=default,DESC") + .param("size", "1").param("page", "0")) + //Verify that the returned item is the one closest to 1990 but below its upperBound (1990-12-31) + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item5, + "Python", "1990") + ))); + + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1990&sort=default,DESC") + .param("size", "3").param("page", "0")) + //Verify that the 3 returned items are from 1990 and below dates, + // with closest to upperBound 1990-12-31 as first + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item5, + "Python", "1990"), + ItemMatcher.matchItemWithTitleAndDateIssued(item2, + "Blade Runner", "1982-06-25"), + ItemMatcher.matchItemWithTitleAndDateIssued(item1, + "Alan Turing", "1912-06-23") + ))); + + getClient().perform(get("/api/discover/browses/dateissued/items?startsWith=1982-06&sort=default,DESC") + .param("size", "1").param("page", "0")) + //Verify that the returned item is the one closest to 1982-06 but below its upperBound (1982-06-30) + .andExpect(jsonPath("$._embedded.items", + contains(ItemMatcher.matchItemWithTitleAndDateIssued(item2, + "Blade Runner", "1982-06-25") ))); } From 11e306fabdbfb1cfcbb1f53c84757ff1a51c0b70 Mon Sep 17 00:00:00 2001 From: Jesiel Viana Date: Mon, 9 Feb 2026 14:52:24 -0300 Subject: [PATCH 523/701] return 404 instead of 401 for deleted bitstreams --- .../java/org/dspace/app/rest/BitstreamRestController.java | 2 +- .../security/AuthorizeServicePermissionEvaluatorPlugin.java | 5 +++++ .../BitstreamMetadataReadPermissionEvaluatorPlugin.java | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java index 8659bd982df4..251bc769cc56 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java @@ -110,7 +110,7 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp Bitstream bit = bitstreamService.find(context, uuid); EPerson currentUser = context.getCurrentUser(); - if (bit == null) { + if (bit == null || bit.isDeleted()) { response.sendError(HttpServletResponse.SC_NOT_FOUND); return null; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java index 3fdd5158ed1b..650b83e719b1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java @@ -15,6 +15,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; @@ -86,6 +87,10 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t return true; } + if (dSpaceObject instanceof Bitstream && ((Bitstream) dSpaceObject).isDeleted()) { + return true; // Let downstream REST layer handle with 404 + } + if (dSpaceObject instanceof Item) { Item item = (Item) dSpaceObject; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/BitstreamMetadataReadPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/BitstreamMetadataReadPermissionEvaluatorPlugin.java index 5a596525e4ce..5b57c7c2842a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/BitstreamMetadataReadPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/BitstreamMetadataReadPermissionEvaluatorPlugin.java @@ -73,6 +73,9 @@ public boolean hasPermission(Authentication authentication, Serializable targetI } public boolean metadataReadPermissionOnBitstream(Context context, Bitstream bitstream) throws SQLException { + if (bitstream.isDeleted()) { + return true; // Let downstream REST layer handle with 404 + } if (authorizeService.isAdmin(context, bitstream)) { // Is Admin on bitstream return true; From eeb82a3d2d31843750b16baf90c4917568fb54f7 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:48:47 -0300 Subject: [PATCH 524/701] Dep: Add Spring LDAP 3.2.15 dependency to replace JNDI (cherry picked from commit 9f638fcee9f0c134737c9526a9e5ea33f7b1af44) --- LICENSES_THIRD_PARTY | 1 + dspace-api/pom.xml | 4 ++++ pom.xml | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index d6b30f3fd1f8..74f966caf188 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -400,6 +400,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.2.14 - https://github.com/spring-projects/spring-framework) * Spring Commons Logging Bridge (org.springframework:spring-jcl:6.2.14 - https://github.com/spring-projects/spring-framework) * Spring JDBC (org.springframework:spring-jdbc:6.2.14 - https://github.com/spring-projects/spring-framework) + * Spring LDAP Core (org.springframework.ldap:spring-ldap-core:3.2.15 - https://github.com/spring-projects/spring-ldap) * Spring Object/Relational Mapping (org.springframework:spring-orm:6.2.14 - https://github.com/spring-projects/spring-framework) * Spring TestContext Framework (org.springframework:spring-test:6.2.14 - https://github.com/spring-projects/spring-framework) * Spring Transaction (org.springframework:spring-tx:6.2.14 - https://github.com/spring-projects/spring-framework) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 61c8ef744f1d..55934752757d 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -379,6 +379,10 @@ ${hibernate-validator.version} + + org.springframework.ldap + spring-ldap-core + org.springframework spring-orm diff --git a/pom.xml b/pom.xml index 48b23ee9a588..9826d2c307bb 100644 --- a/pom.xml +++ b/pom.xml @@ -1200,6 +1200,12 @@ ${spring.version} + + org.springframework.ldap + spring-ldap-core + ${spring-ldap.version} + + spring-tx org.springframework From 36dd8aa307fa106babd60fc1b3ce299eb90a2b65 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:43:00 -0300 Subject: [PATCH 525/701] fix(#10823): Replace JNDI with Spring LDAP in LDAPAuthentication (cherry picked from commit d7cc22362813d8ae89ee149aa5ff4c647e200315) --- .../authenticate/LDAPAuthentication.java | 312 ++++++------------ 1 file changed, 92 insertions(+), 220 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java index 9d7c09f1c314..efb2ee6ca85f 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java @@ -9,27 +9,13 @@ import static org.dspace.eperson.service.EPersonService.MD_PHONE; -import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Optional; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.BasicAttributes; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.InitialLdapContext; -import javax.naming.ldap.LdapContext; -import javax.naming.ldap.StartTlsRequest; -import javax.naming.ldap.StartTlsResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -47,6 +33,14 @@ import org.dspace.eperson.service.GroupService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.springframework.ldap.core.ContextMapper; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.support.LdapUtils; + /** * This combined LDAP authentication method supersedes both the 'LDAPAuthentication' @@ -429,6 +423,7 @@ private static class SpeakerToLDAP { final String ldap_group_field; final boolean useTLS; + private LdapTemplate ldapTemplate; SpeakerToLDAP(Logger thelog) { ConfigurationService configurationService @@ -445,252 +440,129 @@ private static class SpeakerToLDAP { ldap_phone_field = configurationService.getProperty("authentication-ldap.phone_field"); ldap_group_field = configurationService.getProperty("authentication-ldap.login.groupmap.attribute"); useTLS = configurationService.getBooleanProperty("authentication-ldap.starttls", false); + + setupSpringLdap(configurationService); } - protected String getDNOfUser(String adminUser, String adminPassword, Context context, String netid) { - // The resultant DN - String resultDN; + private void setupSpringLdap(ConfigurationService cfg) { + LdapContextSource contextSource = new LdapContextSource(); + contextSource.setUrl(ldap_provider_url); - // The search scope to use (default to 0) - int ldap_search_scope_value = 0; - try { - ldap_search_scope_value = Integer.parseInt(ldap_search_scope.trim()); - } catch (NumberFormatException e) { - // Log the error if it has been set but is invalid - if (ldap_search_scope != null) { - log.warn(LogHelper.getHeader(context, - "ldap_authentication", "invalid search scope: " + ldap_search_scope)); - } - } + String adminUser = cfg.getProperty("authentication-ldap.search.user"); + String adminPass = cfg.getProperty("authentication-ldap.search.password"); - // Set up environment for creating initial context - @SuppressWarnings("UseOfObsoleteCollectionType") - Hashtable env = new Hashtable<>(); - env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - env.put(javax.naming.Context.PROVIDER_URL, ldap_provider_url); + if (StringUtils.isNotBlank(adminUser) && StringUtils.isNotBlank(adminPass)) { + contextSource.setUserDn(adminUser); + contextSource.setPassword(adminPass); + } else { + contextSource.setAnonymousReadOnly(true); + } - LdapContext ctx = null; - StartTlsResponse startTLSResponse = null; + contextSource.setPooled(true); + contextSource.afterPropertiesSet(); + this.ldapTemplate = new LdapTemplate(contextSource); + this.ldapTemplate.setIgnorePartialResultException(true); + } + protected String getDNOfUser(String adminUser, String adminPassword, Context context, String netid) { try { - if ((adminUser != null) && (!adminUser.trim().equals("")) && - (adminPassword != null) && (!adminPassword.trim().equals(""))) { - if (useTLS) { - ctx = new InitialLdapContext(env, null); - // start TLS - startTLSResponse = (StartTlsResponse) ctx - .extendedOperation(new StartTlsRequest()); - - startTLSResponse.negotiate(); - - // perform simple client authentication - ctx.addToEnvironment(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); - ctx.addToEnvironment(javax.naming.Context.SECURITY_PRINCIPAL, - adminUser); - ctx.addToEnvironment(javax.naming.Context.SECURITY_CREDENTIALS, - adminPassword); - } else { - // Use admin credentials for search// Authenticate - env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); - env.put(javax.naming.Context.SECURITY_PRINCIPAL, adminUser); - env.put(javax.naming.Context.SECURITY_CREDENTIALS, adminPassword); - } - } else { - // Use anonymous authentication - env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "none"); - } + EqualsFilter filter = new EqualsFilter(ldap_id_field, netid); - if (ctx == null) { - // Create initial context - ctx = new InitialLdapContext(env, null); - } + log.debug("Searching for user using Spring LDAP filter: {}", filter.toString()); - Attributes matchAttrs = new BasicAttributes(true); - matchAttrs.put(new BasicAttribute(ldap_id_field, netid)); + List foundDNs = ldapTemplate.search( + LdapQueryBuilder.query().base(ldap_search_context).filter(filter), + (ContextMapper) (originalCtx) -> { + DirContextOperations ctx = (DirContextOperations) originalCtx; - // look up attributes - try { - SearchControls ctrls = new SearchControls(); - ctrls.setSearchScope(ldap_search_scope_value); - // Fetch both user attributes '*' (eg. uid, cn) and operational attributes '+' (eg. memberOf) - ctrls.setReturningAttributes(new String[] {"*", "+"}); - - String searchName; - if (useTLS) { - searchName = ldap_search_context; - } else { - searchName = ldap_provider_url + ldap_search_context; - } - @SuppressWarnings("BanJNDI") - NamingEnumeration answer = ctx.search( - searchName, - "(&({0}={1}))", new Object[] {ldap_id_field, - netid}, ctrls); - - while (answer.hasMoreElements()) { - SearchResult sr = answer.next(); - if (StringUtils.isEmpty(ldap_search_context)) { - resultDN = sr.getName(); - } else { - resultDN = (sr.getName() + "," + ldap_search_context); + if (ldap_email_field != null) { + this.ldapEmail = ctx.getStringAttribute(ldap_email_field); } - String attlist[] = {ldap_email_field, ldap_givenname_field, - ldap_surname_field, ldap_phone_field, ldap_group_field}; - Attributes atts = sr.getAttributes(); - Attribute att; - - if (attlist[0] != null) { - att = atts.get(attlist[0]); - if (att != null) { - ldapEmail = (String) att.get(); - } + if (ldap_givenname_field != null) { + this.ldapGivenName = ctx.getStringAttribute(ldap_givenname_field); } - if (attlist[1] != null) { - att = atts.get(attlist[1]); - if (att != null) { - ldapGivenName = (String) att.get(); - } + if (ldap_surname_field != null) { + this.ldapSurname = ctx.getStringAttribute(ldap_surname_field); } - if (attlist[2] != null) { - att = atts.get(attlist[2]); - if (att != null) { - ldapSurname = (String) att.get(); - } + if (ldap_phone_field != null) { + this.ldapPhone = ctx.getStringAttribute(ldap_phone_field); } - if (attlist[3] != null) { - att = atts.get(attlist[3]); - if (att != null) { - ldapPhone = (String) att.get(); - } - } + if (ldap_group_field != null) { + String[] groups = ctx.getStringAttributes(ldap_group_field); + if (groups != null) { - if (attlist[4] != null) { - att = atts.get(attlist[4]); - if (att != null) { - // loop through all groups returned by LDAP - ldapGroup = new ArrayList<>(); - for (NamingEnumeration val = att.getAll(); val.hasMoreElements(); ) { - ldapGroup.add((String) val.next()); - } + this.ldapGroup = new ArrayList<>(Arrays.asList(groups)); } } - if (answer.hasMoreElements()) { - // Oh dear - more than one match - // Ambiguous user, can't continue - - } else { - log.debug(LogHelper.getHeader(context, "got DN", resultDN)); - return resultDN; - } + return ctx.getNameInNamespace(); } - } catch (NamingException e) { - // if the lookup fails go ahead and create a new record for them because the authentication - // succeeded - log.warn(LogHelper.getHeader(context, - "ldap_attribute_lookup", "type=failed_search " - + e)); - } - } catch (NamingException | IOException e) { - log.warn(LogHelper.getHeader(context, - "ldap_authentication", "type=failed_auth " + e)); - } finally { - // Close the context when we're done - try { - if (startTLSResponse != null) { - startTLSResponse.close(); - } - if (ctx != null) { - ctx.close(); - } - } catch (NamingException | IOException e) { - // ignore + ); + + if (foundDNs.isEmpty()) { + log.debug(LogHelper.getHeader(context, "getDNOfUser", "no DN found for user " + netid)); + return null; + } else if (foundDNs.size() > 1) { + log.warn(LogHelper.getHeader(context, "getDNOfUser", "multiple DN found for user " + netid)); + return null; } + String resultDN = foundDNs.get(0); + log.debug(LogHelper.getHeader(context, "got DN", resultDN)); + return resultDN; + } catch (Exception e) { + log.warn(LogHelper.getHeader(context, "ldap_authentication", "type=failed_search " + e)); + return null; } - - // No DN match found - return null; } /** * contact the ldap server and attempt to authenticate */ - protected boolean ldapAuthenticate(String netid, String password, - Context context) { - if (!password.equals("")) { - - LdapContext ctx = null; - StartTlsResponse startTLSResponse = null; + protected boolean ldapAuthenticate(String dn, String password, Context context) { + if (StringUtils.isBlank(password)) { + return false; + } + try { + boolean authenticated = ldapTemplate.authenticate( + LdapUtils.newLdapName(dn), + new EqualsFilter(ldap_id_field, "*").toString(), + password + ); + if (authenticated) { + return true; + } + // Fallback para tentativa direta (útil em alguns cenários de AD) + return verifyPassword(dn, password); - // Set up environment for creating initial context - @SuppressWarnings("UseOfObsoleteCollectionType") - Hashtable env = new Hashtable<>(); - env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, - "com.sun.jndi.ldap.LdapCtxFactory"); - env.put(javax.naming.Context.PROVIDER_URL, ldap_provider_url); + } catch (Exception e) { + log.warn(LogHelper.getHeader(context, "ldap_authentication", "type=failed_auth " + e)); + return false; + } + } - try { - if (useTLS) { - ctx = new InitialLdapContext(env, null); - // start TLS - startTLSResponse = (StartTlsResponse) ctx - .extendedOperation(new StartTlsRequest()); - - startTLSResponse.negotiate(); - - // perform simple client authentication - ctx.addToEnvironment(javax.naming.Context.SECURITY_AUTHENTICATION, "simple"); - ctx.addToEnvironment(javax.naming.Context.SECURITY_PRINCIPAL, - netid); - ctx.addToEnvironment(javax.naming.Context.SECURITY_CREDENTIALS, - password); - ctx.addToEnvironment(javax.naming.Context.AUTHORITATIVE, "true"); - ctx.addToEnvironment(javax.naming.Context.REFERRAL, "follow"); - // dummy operation to check if authentication has succeeded - @SuppressWarnings("BanJNDI") - Attributes trash = ctx.getAttributes(""); - } else if (!useTLS) { - // Authenticate - env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "Simple"); - env.put(javax.naming.Context.SECURITY_PRINCIPAL, netid); - env.put(javax.naming.Context.SECURITY_CREDENTIALS, password); - env.put(javax.naming.Context.AUTHORITATIVE, "true"); - env.put(javax.naming.Context.REFERRAL, "follow"); - - // Try to bind - ctx = new InitialLdapContext(env, null); - } - } catch (NamingException | IOException e) { - // something went wrong (like wrong password) so return false - log.warn(LogHelper.getHeader(context, - "ldap_authentication", "type=failed_auth " + e)); - return false; - } finally { - // Close the context when we're done - try { - if (startTLSResponse != null) { - startTLSResponse.close(); - } - if (ctx != null) { - ctx.close(); - } - } catch (NamingException | IOException e) { - // ignore - } + private boolean verifyPassword(String userDn, String password) { + try { + javax.naming.directory.DirContext ctx = contextSource().getContext(userDn, password); + if (ctx != null) { + LdapUtils.closeContext(ctx); } - } else { + return true; + } catch (Exception e) { return false; } + } - return true; + private LdapContextSource contextSource () { + return (LdapContextSource) ldapTemplate.getContextSource(); } } + /** * Returns the URL of an external login page which is not applicable for this authn method. * From 0984d63dc6d6316f3e074b44a0648c13b7d4494f Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Mon, 8 Dec 2025 17:28:31 -0300 Subject: [PATCH 526/701] Docs: Add comments explaining why BanJNDI is suppressed in safe files (cherry picked from commit c7c0d1b58ab0114bd8885ee5b90b1c745aedfb2c) --- .../java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java | 1 + .../main/java/org/dspace/services/email/EmailServiceImpl.java | 1 + 2 files changed, 2 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java index 88a093c0575d..77bd658b6cd6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java @@ -81,6 +81,7 @@ public void initialize(final ConfigurableApplicationContext applicationContext) * Initially look for JNDI Resource called "java:/comp/env/dspace.dir". * If not found, use value provided in "dspace.dir" in Spring Environment */ + // JNDI usage is safe here as it loads internal DSpace configuration, not user input. @SuppressWarnings("BanJNDI") private String getDSpaceHome(ConfigurableEnvironment environment) { // Load the "dspace.dir" property from Spring Boot's Configuration (application.properties) diff --git a/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java b/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java index b8de1c79994a..56335c1a542d 100644 --- a/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java +++ b/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java @@ -62,6 +62,7 @@ public Session getSession() { } @PostConstruct + // JNDI usage is safe here as it looks up a configured mail session resource, not user input. @SuppressWarnings("BanJNDI") public void init() { // See if there is already a Session in our environment From fe53e36d3f9cf04049221ccad5acde4639d43b70 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:11:18 -0300 Subject: [PATCH 527/701] Fix: Add Spring exclusions to LDAP dependency to prevent classpath conflicts (cherry picked from commit bdb1c94094c5eeaa10ee75fa5ddec4261624d522) --- dspace-api/pom.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 55934752757d..29451a02e56d 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -382,6 +382,24 @@ org.springframework.ldap spring-ldap-core + + + org.springframework + spring-core + + + org.springframework + spring-beans + + + org.springframework + spring-tx + + + org.springframework + spring-context + + org.springframework From 8b7efb55d1148cf3d75e64c3e05c97b3d9467b85 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:14:59 -0300 Subject: [PATCH 528/701] Fix: Address code review (DN escaping, PresentFilter, unused methods) (cherry picked from commit 0fc2565c8b27fa9d5fcb0a93ffc3d748135359c6) --- .../authenticate/LDAPAuthentication.java | 47 +++++++------------ 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java index efb2ee6ca85f..7475c0f432ce 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java @@ -38,7 +38,9 @@ import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.support.LdapContextSource; import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.filter.PresentFilter; import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.support.LdapNameBuilder; import org.springframework.ldap.support.LdapUtils; @@ -232,12 +234,18 @@ public int authenticate(Context context, String idField = configurationService.getProperty("authentication-ldap.id_field"); String dn = ""; - // If adminUser is blank and anonymous search is not allowed, then we can't search so construct the DN - // instead of searching it if ((StringUtils.isBlank(adminUser) || StringUtils.isBlank(adminPassword)) && !anonymousSearch) { - dn = idField + "=" + netid + "," + objectContext; + try { + dn = LdapNameBuilder.newInstance(objectContext) + .add(idField, netid) + .build() + .toString(); + } catch (Exception e) { + log.warn("Failed to build DN for user " + netid, e); + return BAD_ARGS; + } } else { - dn = ldap.getDNOfUser(adminUser, adminPassword, context, netid); + dn = ldap.getDNOfUser(context, netid); } // Check a DN was found @@ -446,6 +454,9 @@ private static class SpeakerToLDAP { private void setupSpringLdap(ConfigurationService cfg) { LdapContextSource contextSource = new LdapContextSource(); + if (StringUtils.isBlank(ldap_provider_url)) { + log.error("LDAP provider URL is empty! Check authentication-ldap.provider_url"); + } contextSource.setUrl(ldap_provider_url); String adminUser = cfg.getProperty("authentication-ldap.search.user"); @@ -464,7 +475,7 @@ private void setupSpringLdap(ConfigurationService cfg) { this.ldapTemplate.setIgnorePartialResultException(true); } - protected String getDNOfUser(String adminUser, String adminPassword, Context context, String netid) { + protected String getDNOfUser(Context context, String netid) { try { EqualsFilter filter = new EqualsFilter(ldap_id_field, netid); @@ -494,11 +505,9 @@ protected String getDNOfUser(String adminUser, String adminPassword, Context con if (ldap_group_field != null) { String[] groups = ctx.getStringAttributes(ldap_group_field); if (groups != null) { - this.ldapGroup = new ArrayList<>(Arrays.asList(groups)); } } - return ctx.getNameInNamespace(); } ); @@ -530,36 +539,16 @@ protected boolean ldapAuthenticate(String dn, String password, Context context) try { boolean authenticated = ldapTemplate.authenticate( LdapUtils.newLdapName(dn), - new EqualsFilter(ldap_id_field, "*").toString(), + new PresentFilter(ldap_id_field).toString(), password ); - if (authenticated) { - return true; - } - // Fallback para tentativa direta (útil em alguns cenários de AD) - return verifyPassword(dn, password); + return authenticated; } catch (Exception e) { log.warn(LogHelper.getHeader(context, "ldap_authentication", "type=failed_auth " + e)); return false; } } - - private boolean verifyPassword(String userDn, String password) { - try { - javax.naming.directory.DirContext ctx = contextSource().getContext(userDn, password); - if (ctx != null) { - LdapUtils.closeContext(ctx); - } - return true; - } catch (Exception e) { - return false; - } - } - - private LdapContextSource contextSource () { - return (LdapContextSource) ldapTemplate.getContextSource(); - } } From f8404079d1186e00443ab0e0dddebf082d41eff2 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Tue, 16 Dec 2025 17:36:10 -0300 Subject: [PATCH 529/701] Fix: Address code review (throw exception on missing URL) (cherry picked from commit 4098b35e29041bb2c69d21237ae9b8f88bbe1efb) --- .../main/java/org/dspace/authenticate/LDAPAuthentication.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java index 7475c0f432ce..f516645dfc41 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java @@ -455,7 +455,9 @@ private static class SpeakerToLDAP { private void setupSpringLdap(ConfigurationService cfg) { LdapContextSource contextSource = new LdapContextSource(); if (StringUtils.isBlank(ldap_provider_url)) { - log.error("LDAP provider URL is empty! Check authentication-ldap.provider_url"); + throw new IllegalStateException( + "LDAP provider URL is empty! Please check 'authentication-ldap.provider_url' in your configuration." + ); } contextSource.setUrl(ldap_provider_url); From e515b41069597566e3379ae503b2f8afb103867e Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:28:09 -0300 Subject: [PATCH 530/701] Fix: Upgrade Spring LDAP to 3.3.5 and disable LDAP health check in tests - Upgraded to 3.3.5 to resolve classpath conflicts with Spring Boot 3.5.x. - Removed dependency exclusions in to restore required by Actuator auto-configuration. - Added explicit dependencies to parent POM to resolve convergence errors. - Disabled in to prevent HealthIndicatorsIT failure (DOWN status) due to missing LDAP server in test environment. (cherry picked from commit 637c28eed8f4cc7e1596a325d72b1e97d44fe655) --- dspace-api/pom.xml | 18 ------------------ .../test/resources/application-test.properties | 5 ++++- pom.xml | 11 +++++++++++ 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 29451a02e56d..55934752757d 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -382,24 +382,6 @@ org.springframework.ldap spring-ldap-core - - - org.springframework - spring-core - - - org.springframework - spring-beans - - - org.springframework - spring-tx - - - org.springframework - spring-context - - org.springframework diff --git a/dspace-server-webapp/src/test/resources/application-test.properties b/dspace-server-webapp/src/test/resources/application-test.properties index bd9e2ea4a17b..ca065187d46c 100644 --- a/dspace-server-webapp/src/test/resources/application-test.properties +++ b/dspace-server-webapp/src/test/resources/application-test.properties @@ -16,5 +16,8 @@ ## This file is found on classpath at src/test/resources/log4j2-test.xml logging.config = classpath:log4j2-test.xml +# Disable LDAP Health Check during tests to avoid external LDAP requirement +management.health.ldap.enabled=false + # Our integration tests expect application to be deployed at the root path (/) -server.servlet.context-path=/ \ No newline at end of file +server.servlet.context-path=/ diff --git a/pom.xml b/pom.xml index 9826d2c307bb..187995ded8fc 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ 17 6.2.15 + 3.3.5 3.5.10 6.5.7 6.4.10.Final @@ -1205,6 +1206,16 @@ spring-ldap-core ${spring-ldap.version} + + io.micrometer + micrometer-core + 1.14.14 + + + io.micrometer + micrometer-observation + 1.14.14 + spring-tx From da70567fa0ed315e29b0a0c2ecac53c552d5af31 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:35:10 -0300 Subject: [PATCH 531/701] Docs: Add comment explaining micrometer dependency usage (cherry picked from commit 8401ff24f0712a3006ab76ba784d871d2431f7d0) --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 187995ded8fc..259b29feae13 100644 --- a/pom.xml +++ b/pom.xml @@ -1205,7 +1205,8 @@ org.springframework.ldap spring-ldap-core ${spring-ldap.version} - + + io.micrometer micrometer-core From 816dfbe473effcdc4b6df210e79d3be6d6c7d3bb Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 13 Feb 2026 09:49:22 -0600 Subject: [PATCH 532/701] Fix broken tests due to random order execution. When CreateMissingIdentifiersIT is run before WorkflowCurationIT, it was causing the latter to fail. --- .../org/dspace/ctask/general/CreateMissingIdentifiersIT.java | 2 ++ .../test/java/org/dspace/workflow/WorkflowCurationIT.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java b/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java index 3b50258a5a23..8ecddc6cd50c 100644 --- a/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java +++ b/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java @@ -45,6 +45,7 @@ public void testPerform() // Must remove any cached named plugins before creating a new one CoreServiceFactory.getInstance().getPluginService().clearNamedPluginClasses(); // Define a new task dynamically + String[] prevTaskDef = configurationService.getArrayProperty(P_TASK_DEF); configurationService.setProperty(P_TASK_DEF, CreateMissingIdentifiers.class.getCanonicalName() + " = " + TASK_NAME); @@ -82,5 +83,6 @@ public void testPerform() curator.curate(context, item); int status = curator.getStatus(TASK_NAME); assertEquals("Curation should succeed", Curator.CURATE_SUCCESS, status); + configurationService.setProperty(P_TASK_DEF, prevTaskDef); } } diff --git a/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java b/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java index d3866d534b24..37124bf6bc9c 100644 --- a/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java +++ b/dspace-api/src/test/java/org/dspace/workflow/WorkflowCurationIT.java @@ -22,6 +22,7 @@ import org.dspace.content.Community; import org.dspace.content.MetadataValue; import org.dspace.content.service.ItemService; +import org.dspace.core.LegacyPluginServiceImpl; import org.dspace.ctask.testing.MarkerTask; import org.dspace.eperson.EPerson; import org.dspace.util.DSpaceConfigurationInitializer; @@ -29,6 +30,7 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -46,6 +48,8 @@ public class WorkflowCurationIT extends AbstractIntegrationTestWithDatabase { @Inject private ItemService itemService; + @Autowired + private LegacyPluginServiceImpl legacyPluginService; /** * Basic smoke test of a curation task attached to a workflow step. @@ -56,6 +60,7 @@ public class WorkflowCurationIT public void curationTest() throws Exception { context.turnOffAuthorisationSystem(); + legacyPluginService.clearNamedPluginClasses(); //** GIVEN ** From 88e15b89f0a9e54c54cfcd9e0f1f105a24f438cb Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 12 Feb 2026 16:11:25 -0600 Subject: [PATCH 533/701] Fix failing S3BitStoreServiceIT by upgrading testcontainers 2.0.3. Avoids a bug in testcontainers 2.0.2 when certain versions of Docker are found on path. (cherry picked from commit 005bbf08b7ca530381db22f9a52bb0761e5eb8dd) --- dspace-api/pom.xml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index fef4d52cb1bf..22b752dd9e1e 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -852,10 +852,25 @@ - com.adobe.testing - s3mock-testcontainers - 4.11.0 - test + com.adobe.testing + s3mock-testcontainers + 4.11.0 + test + + + org.testcontainers + testcontainers + + + + + + org.testcontainers + testcontainers + 2.0.3 + test From 796e64099610ff682e1a1d597434ada7f20f6443 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 9 Sep 2025 10:01:29 +0200 Subject: [PATCH 534/701] Set eperson attributes with fallback email from netid_email_domain when ldap email does not exist (cherry picked from commit 6a10ba7c8b89ce1cf0e7a1ee8d3141103e854879) --- .../authenticate/LDAPAuthentication.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java index f516645dfc41..b199cdb94685 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/LDAPAuthentication.java @@ -324,7 +324,7 @@ public int authenticate(Context context, log.info(LogHelper.getHeader(context, "type=ldap-login", "type=ldap_but_already_email")); context.turnOffAuthorisationSystem(); - setEpersonAttributes(context, eperson, ldap, Optional.of(netid)); + setEpersonAttributes(context, eperson, ldap, Optional.of(netid), email); ePersonService.update(context, eperson); context.dispatchEvents(); context.restoreAuthSystemState(); @@ -341,7 +341,7 @@ public int authenticate(Context context, try { context.turnOffAuthorisationSystem(); eperson = ePersonService.create(context); - setEpersonAttributes(context, eperson, ldap, Optional.of(netid)); + setEpersonAttributes(context, eperson, ldap, Optional.of(netid), email); eperson.setCanLogIn(true); authenticationService.initEPerson(context, request, eperson); ePersonService.update(context, eperson); @@ -383,11 +383,24 @@ public int authenticate(Context context, * Update eperson's attributes */ private void setEpersonAttributes(Context context, EPerson eperson, SpeakerToLDAP ldap, Optional netid) - throws SQLException { + throws SQLException { + setEpersonAttributes(context, eperson, ldap, netid, null); + } + /** + * Update eperson's attributes + */ + private void setEpersonAttributes(Context context, EPerson eperson, SpeakerToLDAP ldap, Optional netid, + String email) + throws SQLException { + + // Set email address: first try LDAP email, then fallback to provided email parameter if (StringUtils.isNotEmpty(ldap.ldapEmail)) { eperson.setEmail(ldap.ldapEmail); + } else if (StringUtils.isNotEmpty(email)) { + eperson.setEmail(email); } + if (StringUtils.isNotEmpty(ldap.ldapGivenName)) { eperson.setFirstName(context, ldap.ldapGivenName); } From 2753036cc1aeef89231f37dcac8cc6119ec78dc5 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Sat, 29 Nov 2025 22:10:34 +0100 Subject: [PATCH 535/701] Update community indexing to include all ancestor communities, not just direct parents --- .../dspace/discovery/indexobject/CommunityIndexFactoryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/CommunityIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/CommunityIndexFactoryImpl.java index e92819601839..9f4b558cce76 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/CommunityIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/CommunityIndexFactoryImpl.java @@ -126,7 +126,7 @@ public List getLocations(Context context, IndexableCommunity indexableDS final Community target = indexableDSpaceObject.getIndexedObject(); List locations = new ArrayList<>(); // build list of community ids - List communities = target.getParentCommunities(); + List communities = communityService.getAllParents(context, target); // now put those into strings for (Community community : communities) { From 68d8e883afd03196799a5fd3e2e37b4183102a14 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Sat, 29 Nov 2025 22:14:56 +0100 Subject: [PATCH 536/701] Replace separate community/collection admin checks with unified isComColAdmin() call --- .../org/dspace/discovery/SolrServicePrivateItemPlugin.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServicePrivateItemPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServicePrivateItemPlugin.java index db543141e13d..aab1176d5af5 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServicePrivateItemPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServicePrivateItemPlugin.java @@ -45,9 +45,8 @@ public void additionalSearchParameters(Context context, DiscoverQuery discoveryQ solrQuery.addFilterQuery("NOT(discoverable:false)"); return; } - if (!authorizeService.isCommunityAdmin(context) && !authorizeService.isCollectionAdmin(context)) { + if (!authorizeService.isComColAdmin(context)) { solrQuery.addFilterQuery("NOT(discoverable:false)"); - } } catch (SQLException ex) { log.error(LogHelper.getHeader(context, "Error looking up authorization rights of current user", From 3f2dbbbb296d239190121400c9c843ef104a8270 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Sat, 29 Nov 2025 22:26:09 +0100 Subject: [PATCH 537/701] Refactor permission indexing to store only direct object permissions and update Solr filter query logic to resolve inherited permissions via the location ancestor field; add authorization-based endpoints for Item, Collection, and Community. --- .../authorize/AuthorizeServiceImpl.java | 146 +++++++++------ .../authorize/service/AuthorizeService.java | 58 +++++- .../dspace/content/CollectionServiceImpl.java | 38 ++-- .../org/dspace/content/ItemServiceImpl.java | 28 ++- .../content/service/CollectionService.java | 21 ++- .../dspace/content/service/ItemService.java | 10 +- .../org/dspace/discovery/DiscoverQuery.java | 59 +++++++ .../org/dspace/discovery/SearchService.java | 2 + .../org/dspace/discovery/SolrServiceImpl.java | 52 ++++++ .../SolrServiceResourceRestrictionPlugin.java | 166 +++++++++++------- .../dspace/content/service/ItemServiceIT.java | 20 +-- .../authorization/impl/EditItemFeature.java | 2 +- .../authorization/impl/SubmitFeature.java | 2 +- .../repository/CollectionRestRepository.java | 39 ++-- .../repository/CommunityRestRepository.java | 24 ++- .../rest/repository/ItemRestRepository.java | 24 +++ dspace/config/spring/api/discovery.xml | 2 - 17 files changed, 487 insertions(+), 206 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java index 60004bda9538..74ad6f6e82b1 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -742,15 +742,15 @@ public List getPoliciesActionFilterExceptRpType(Context c, DSpac /** * Checks that the context's current user is a community admin in the site by querying the solr database. + * This query doesn't use authorization inheritance because direct community admin is enough to perform this check. * * @param context context with the current user * @return true if the current user is a community admin in the site * false when this is not the case, or an exception occurred - * @throws java.sql.SQLException passed through. */ @Override - public boolean isCommunityAdmin(Context context) throws SQLException { - return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE); + public boolean isCommunityAdmin(Context context) { + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, false); } /** @@ -759,11 +759,10 @@ public boolean isCommunityAdmin(Context context) throws SQLException { * @param context context with the current user * @return true if the current user is a collection admin in the site * false when this is not the case, or an exception occurred - * @throws java.sql.SQLException passed through. */ @Override - public boolean isCollectionAdmin(Context context) throws SQLException { - return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE); + public boolean isCollectionAdmin(Context context) { + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, true); } /** @@ -772,26 +771,26 @@ public boolean isCollectionAdmin(Context context) throws SQLException { * @param context context with the current user * @return true if the current user is an item admin in the site * false when this is not the case, or an exception occurred - * @throws java.sql.SQLException passed through. */ @Override - public boolean isItemAdmin(Context context) throws SQLException { - return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE); + public boolean isItemAdmin(Context context) { + return performCheck(context, RESOURCE_TYPE_FIELD + ":" + IndexableItem.TYPE, true); } /** * Checks that the context's current user is a community or collection admin in the site. + * This query doesn't use authorization inheritance because direct community/collection admin is enough to + * perform this check. * * @param context context with the current user * @return true if the current user is a community or collection admin in the site * false when this is not the case, or an exception occurred - * @throws java.sql.SQLException passed through. */ @Override - public boolean isComColAdmin(Context context) throws SQLException { + public boolean isComColAdmin(Context context) { return performCheck(context, "(" + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE + " OR " + - RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE + ")"); + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE + ")", false); } /** @@ -806,11 +805,29 @@ public boolean isComColAdmin(Context context) throws SQLException { */ @Override public List findAdminAuthorizedCommunity(Context context, String query, int offset, int limit) - throws SearchServiceException, SQLException { + throws SearchServiceException { + return findAuthorizedCommunityByAction(context, query, Constants.ADMIN, offset, limit); + } + + /** + * Finds communities for which the logged in user has the rights specified by the action parameter. + * + * @param context the context whose user is checked against + * @param query the optional extra query + * @param action the action to check for + * @param offset the offset for pagination + * @param limit the amount of dso's to return + * @return a list of communities for which the logged in user has the rights specified by the action + * @throws SearchServiceException + */ + @Override + public List findAuthorizedCommunityByAction(Context context, String query, int action, int offset, + int limit) + throws SearchServiceException { List communities = new ArrayList<>(); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + - IndexableCommunity.TYPE, + IndexableCommunity.TYPE, action, true, offset, limit, null, null); for (IndexableObject solrCollections : discoverResult.getIndexableObjects()) { Community community = ((IndexableCommunity) solrCollections).getIndexedObject(); @@ -829,10 +846,25 @@ public List findAdminAuthorizedCommunity(Context context, String quer */ @Override public long countAdminAuthorizedCommunity(Context context, String query) - throws SearchServiceException, SQLException { + throws SearchServiceException { + return countAuthorizedCommunityByAction(context, query, Constants.ADMIN); + } + + /** + * Counts communities for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @return the matching communities + * @throws SearchServiceException + */ + @Override + public long countAuthorizedCommunityByAction(Context context, String query, int action) + throws SearchServiceException { query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + - IndexableCommunity.TYPE, + IndexableCommunity.TYPE, action, true, null, 0, null, null); return discoverResult.getTotalSearchResults(); } @@ -849,7 +881,25 @@ public long countAdminAuthorizedCommunity(Context context, String query) */ @Override public List findAdminAuthorizedCollection(Context context, String query, int offset, int limit) - throws SearchServiceException, SQLException { + throws SearchServiceException { + return findAuthorizedCollectionByAction(context, query, Constants.ADMIN, offset, limit); + } + + /** + * Finds collections for which the logged in user has the rights specified by the action parameter. + * + * @param context the context whose user is checked against + * @param query the optional extra query + * @param action the action to check for + * @param offset the offset for pagination + * @param limit the amount of dso's to return + * @return a list of collections for which the logged in user has the rights specified by the action + * @throws SearchServiceException + */ + @Override + public List findAuthorizedCollectionByAction(Context context, String query, + int action, int offset, int limit) + throws SearchServiceException { List collections = new ArrayList<>(); if (context.getCurrentUser() == null) { return collections; @@ -857,7 +907,7 @@ public List findAdminAuthorizedCollection(Context context, String qu query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + - IndexableCollection.TYPE, + IndexableCollection.TYPE, action, true, offset, limit, CollectionService.SOLR_SORT_FIELD, SORT_ORDER.asc); for (IndexableObject solrCollections : discoverResult.getIndexableObjects()) { Collection collection = ((IndexableCollection) solrCollections).getIndexedObject(); @@ -876,31 +926,43 @@ public List findAdminAuthorizedCollection(Context context, String qu */ @Override public long countAdminAuthorizedCollection(Context context, String query) - throws SearchServiceException, SQLException { + throws SearchServiceException { + return countAuthorizedCollectionByAction(context, query, Constants.ADMIN); + } + + /** + * Counts collections for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @return the matching collections + * @throws SearchServiceException + */ + @Override + public long countAuthorizedCollectionByAction(Context context, String query, int action) + throws SearchServiceException { query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + - IndexableCollection.TYPE, - null, 0, null, null); + IndexableCollection.TYPE, action, + true, null, 0, null, null); return discoverResult.getTotalSearchResults(); } @Override public boolean isAccountManager(Context context) { - try { - return (canCommunityAdminManageAccounts() && isCommunityAdmin(context) - || canCollectionAdminManageAccounts() && isCollectionAdmin(context)); - } catch (SQLException e) { - throw new RuntimeException(e); - } + return (canCommunityAdminManageAccounts() && isCommunityAdmin(context) + || canCollectionAdminManageAccounts() && isCollectionAdmin(context)); } - private boolean performCheck(Context context, String query) throws SQLException { + private boolean performCheck(Context context, String query, boolean inheritAuthorizations) { if (context.getCurrentUser() == null) { return false; } try { - DiscoverResult discoverResult = getDiscoverResult(context, query, null, null, null, null); + DiscoverResult discoverResult = getDiscoverResult(context, query, Constants.ADMIN, inheritAuthorizations, + null, 0, null, null); if (discoverResult.getTotalSearchResults() > 0) { return true; } @@ -912,16 +974,11 @@ private boolean performCheck(Context context, String query) throws SQLException return false; } - private DiscoverResult getDiscoverResult(Context context, String query, Integer offset, Integer limit, - String sortField, SORT_ORDER sortOrder) - throws SearchServiceException, SQLException { - String groupQuery = getGroupToQuery(groupService.allMemberGroups(context, context.getCurrentUser())); + private DiscoverResult getDiscoverResult(Context context, String query, int action, boolean inheritAuthorizations, + Integer offset, Integer limit, String sortField, SORT_ORDER sortOrder) + throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); - if (!this.isAdmin(context)) { - query = query + " AND (" + - "admin:e" + context.getCurrentUser().getID() + groupQuery + ")"; - } discoverQuery.setQuery(query); if (offset != null) { discoverQuery.setStart(offset); @@ -932,23 +989,12 @@ private DiscoverResult getDiscoverResult(Context context, String query, Integer if (sortField != null && sortOrder != null) { discoverQuery.setSortField(sortField, sortOrder); } + discoverQuery.addRequiredAuthorization(action); + discoverQuery.setInheritAuthorizations(inheritAuthorizations); return searchService.search(context, discoverQuery); } - private String getGroupToQuery(List groups) { - StringBuilder groupQuery = new StringBuilder(); - - if (groups != null) { - for (Group group: groups) { - groupQuery.append(" OR admin:g"); - groupQuery.append(group.getID()); - } - } - - return groupQuery.toString(); - } - private String formatCustomQuery(String query) { if (StringUtils.isBlank(query)) { return ""; diff --git a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java index 0e542d98f6d4..95e4ec1ee627 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java +++ b/dspace-api/src/main/java/org/dspace/authorize/service/AuthorizeService.java @@ -559,6 +559,20 @@ void switchPoliciesAction(Context context, DSpaceObject dso, int fromAction, int List findAdminAuthorizedCommunity(Context context, String query, int offset, int limit) throws SearchServiceException, SQLException; + /** + * Finds communities for which the logged in user has the rights specified by the action parameter. + * + * @param context the context whose user is checked against + * @param query the optional extra query + * @param action the action to check for + * @param offset the offset for pagination + * @param limit the amount of dso's to return + * @return a list of communities for which the logged in user has the rights specified by the action + * @throws SearchServiceException + */ + List findAuthorizedCommunityByAction(Context context, String query, int action, int offset, int limit) + throws SearchServiceException, SQLException; + /** * Counts communities for which the current user is admin, AND which match the query. * @@ -571,6 +585,18 @@ List findAdminAuthorizedCommunity(Context context, String query, int long countAdminAuthorizedCommunity(Context context, String query) throws SearchServiceException, SQLException; + /** + * Counts communities for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @return the matching communities + * @throws SearchServiceException + */ + long countAuthorizedCommunityByAction(Context context, String query, int action) + throws SearchServiceException; + /** * Finds collections for which the current user is admin, AND which match the query. * @@ -580,10 +606,24 @@ long countAdminAuthorizedCommunity(Context context, String query) * @param limit used for pagination of the results * @return the matching collections * @throws SearchServiceException - * @throws SQLException */ List findAdminAuthorizedCollection(Context context, String query, int offset, int limit) - throws SearchServiceException, SQLException; + throws SearchServiceException; + + /** + * Finds collections for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @param offset used for pagination of the results + * @param limit used for pagination of the results + * @return the matching collections + * @throws SearchServiceException + */ + List findAuthorizedCollectionByAction(Context context, String query, int action, int offset, + int limit) + throws SearchServiceException; /** * Counts collections for which the current user is admin, AND which match the query. @@ -595,7 +635,19 @@ List findAdminAuthorizedCollection(Context context, String query, in * @throws SQLException */ long countAdminAuthorizedCollection(Context context, String query) - throws SearchServiceException, SQLException; + throws SearchServiceException; + + /** + * Counts collections for which the current user has the rights specified by the action parameter. + * + * @param context context with the current user + * @param query the query for which to filter the results more + * @param action the action to check for + * @return the number of matching collections + * @throws SearchServiceException + */ + long countAuthorizedCollectionByAction(Context context, String query, int action) + throws SearchServiceException; /** * Returns true if the current user can manage accounts. diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index 10f458b81975..7531f748adf4 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -1033,7 +1033,7 @@ public String getDefaultReadGroupName(Collection collection, String typeOfGroupS @Override public List findCollectionsWithSubmit(String q, Context context, Community community, - int offset, int limit) throws SQLException, SearchServiceException { + int offset, int limit) throws SearchServiceException { List collections = new ArrayList<>(); DiscoverQuery discoverQuery = new DiscoverQuery(); @@ -1050,8 +1050,8 @@ public List findCollectionsWithSubmit(String q, Context context, Com } @Override - public int countCollectionsWithSubmit(String q, Context context, Community community) - throws SQLException, SearchServiceException { + public int countCollectionsWithSubmit(Context context, String q, Community community) + throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setMaxResults(0); @@ -1073,29 +1073,12 @@ public int countCollectionsWithSubmit(String q, Context context, Community commu * terms. The terms are used to make also a prefix query on SOLR * so it can be used to implement an autosuggest feature over the collection name * @return discovery search result objects - * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ private DiscoverResult retrieveCollectionsWithSubmit(Context context, DiscoverQuery discoverQuery, String entityType, Community community, String q) - throws SQLException, SearchServiceException { - - StringBuilder query = new StringBuilder(); - EPerson currentUser = context.getCurrentUser(); - if (!authorizeService.isAdmin(context)) { - String userId = ""; - if (currentUser != null) { - userId = currentUser.getID().toString(); - } - query.append("submit:(e").append(userId); + throws SearchServiceException { - Set groups = groupService.allMemberGroupsSet(context, currentUser); - for (Group group : groups) { - query.append(" OR g").append(group.getID()); - } - query.append(")"); - discoverQuery.addFilterQueries(query.toString()); - } if (Objects.nonNull(community)) { discoverQuery.addFilterQueries("location.comm:" + community.getID().toString()); } @@ -1109,6 +1092,7 @@ private DiscoverResult retrieveCollectionsWithSubmit(Context context, DiscoverQu .append(escapedQuery).append("*").append(")"); discoverQuery.setQuery(buildQuery.toString()); } + discoverQuery.addRequiredAuthorization(Constants.ADD); DiscoverResult resp = searchService.search(context, discoverQuery); return resp; } @@ -1148,8 +1132,8 @@ public Collection retrieveCollectionWithSubmitByCommunityAndEntityType(Context c context.turnOffAuthorisationSystem(); List collections; try { - collections = findCollectionsWithSubmit(null, context, community, entityType, 0, 1); - } catch (SQLException | SearchServiceException e) { + collections = findCollectionsWithSubmit(context, null, community, entityType, 0, 1); + } catch (SearchServiceException e) { throw new RuntimeException(e); } context.restoreAuthSystemState(); @@ -1169,8 +1153,8 @@ public Collection retrieveCollectionWithSubmitByCommunityAndEntityType(Context c } @Override - public List findCollectionsWithSubmit(String q, Context context, Community community, String entityType, - int offset, int limit) throws SQLException, SearchServiceException { + public List findCollectionsWithSubmit(Context context, String q, Community community, String entityType, + int offset, int limit) throws SearchServiceException { List collections = new ArrayList<>(); DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE); @@ -1187,8 +1171,8 @@ public List findCollectionsWithSubmit(String q, Context context, Com } @Override - public int countCollectionsWithSubmit(String q, Context context, Community community, String entityType) - throws SQLException, SearchServiceException { + public int countCollectionsWithSubmit(Context context, String q, Community community, String entityType) + throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setMaxResults(0); discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE); diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 998cde590e78..23e08800a9d4 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -22,7 +22,6 @@ import java.util.UUID; import java.util.function.Supplier; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -1261,43 +1260,38 @@ public boolean canEdit(Context context, Item item) throws SQLException { * * @param context DSpace context * @param discoverQuery + * @param q query string * @return discovery search result objects - * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ - private DiscoverResult retrieveItemsWithEdit(Context context, DiscoverQuery discoverQuery) - throws SQLException, SearchServiceException { - EPerson currentUser = context.getCurrentUser(); - if (!authorizeService.isAdmin(context)) { - String userId = currentUser != null ? "e" + currentUser.getID().toString() : "e"; - Stream groupIds = groupService.allMemberGroupsSet(context, currentUser).stream() - .map(group -> "g" + group.getID()); - String query = Stream.concat(Stream.of(userId), groupIds) - .collect(Collectors.joining(" OR ", "edit:(", ")")); - discoverQuery.addFilterQueries(query); + private DiscoverResult retrieveItemsWithEdit(Context context, DiscoverQuery discoverQuery, String q) + throws SearchServiceException { + if (StringUtils.isNotBlank(q)) { + discoverQuery.setQuery(q); } + discoverQuery.addRequiredAuthorization(Constants.WRITE); return searchService.search(context, discoverQuery); } @Override - public List findItemsWithEdit(Context context, int offset, int limit) - throws SQLException, SearchServiceException { + public List findItemsWithEdit(Context context, String q, int offset, int limit) + throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE); discoverQuery.setStart(offset); discoverQuery.setMaxResults(limit); - DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery); + DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery, q); return resp.getIndexableObjects().stream() .map(solrItems -> ((IndexableItem) solrItems).getIndexedObject()) .collect(Collectors.toList()); } @Override - public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException { + public int countItemsWithEdit(Context context, String q) throws SearchServiceException { DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setMaxResults(0); discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE); - DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery); + DiscoverResult resp = retrieveItemsWithEdit(context, discoverQuery, q); return (int) resp.getTotalSearchResults(); } diff --git a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java index c2b633821376..5e81488bf1ce 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java @@ -391,11 +391,11 @@ Group createDefaultReadGroup(Context context, Collection collection, String type * NOTE: for better performance, this method retrieves its results from an * index (cache) and does not query the database directly. * This means that results may be stale or outdated until https://github.com/DSpace/DSpace/issues/2853 is resolved" - * + * + * @param context DSpace Context * @param q limit the returned collection to those with metadata values matching the query terms. * The terms are used to make also a prefix query on SOLR so it can be used to implement * an autosuggest feature over the collection name - * @param context DSpace Context * @param community parent community * @param entityType limit the returned collection to those related to given entity type * @param offset the position of the first result to return @@ -404,7 +404,7 @@ Group createDefaultReadGroup(Context context, Collection collection, String type * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ - public List findCollectionsWithSubmit(String q, Context context, Community community, + public List findCollectionsWithSubmit(Context context, String q, Community community, String entityType, int offset, int limit) throws SQLException, SearchServiceException; /** @@ -422,11 +422,10 @@ public List findCollectionsWithSubmit(String q, Context context, Com * @param offset the position of the first result to return * @param limit paging limit * @return discovery search result objects - * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ public List findCollectionsWithSubmit(String q, Context context, Community community, - int offset, int limit) throws SQLException, SearchServiceException; + int offset, int limit) throws SearchServiceException; /** * Retrieve the first collection in the community or its descending that support @@ -462,17 +461,17 @@ public Collection retrieveCollectionWithSubmitByEntityType(Context context, Item * and does not query the database directly. * This means that results may be stale or outdated until * https://github.com/DSpace/DSpace/issues/2853 is resolved." - * + * + * @param context DSpace Context * @param q limit the returned collection to those with metadata values matching the query terms. * The terms are used to make also a prefix query on SOLR so it can be used to implement * an autosuggest feature over the collection name - * @param context DSpace Context * @param community parent community * @return total collections found * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ - public int countCollectionsWithSubmit(String q, Context context, Community community) + public int countCollectionsWithSubmit(Context context, String q, Community community) throws SQLException, SearchServiceException; /** @@ -481,18 +480,18 @@ public int countCollectionsWithSubmit(String q, Context context, Community commu * and does not query the database directly. * This means that results may be stale or outdated until * https://github.com/DSpace/DSpace/issues/2853 is resolved." - * + * + * @param context DSpace Context * @param q limit the returned collection to those with metadata values matching the query terms. * The terms are used to make also a prefix query on SOLR so it can be used to implement * an autosuggest feature over the collection name - * @param context DSpace Context * @param community parent community * @param entityType limit the returned collection to those related to given entity type * @return total collections found * @throws SQLException if something goes wrong * @throws SearchServiceException if search error */ - public int countCollectionsWithSubmit(String q, Context context, Community community, String entityType) + public int countCollectionsWithSubmit(Context context, String q, Community community, String entityType) throws SQLException, SearchServiceException; /** diff --git a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java index 3fea75665bcb..7c202a4837f3 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java @@ -915,23 +915,23 @@ Iterator findByLastModifiedSince(Context context, Date last) /** * finds all items for which the current user has editing rights * @param context DSpace context object + * @param q search query * @param offset page offset * @param limit page size limit * @return list of items for which the current user has editing rights - * @throws SQLException * @throws SearchServiceException */ - List findItemsWithEdit(Context context, int offset, int limit) - throws SQLException, SearchServiceException; + List findItemsWithEdit(Context context, String q, int offset, int limit) + throws SearchServiceException; /** * counts all items for which the current user has editing rights * @param context DSpace context object + * @param q search query * @return list of items for which the current user has editing rights - * @throws SQLException * @throws SearchServiceException */ - int countItemsWithEdit(Context context) throws SQLException, SearchServiceException; + int countItemsWithEdit(Context context, String q) throws SearchServiceException; /** * Check if the supplied item is an inprogress submission diff --git a/dspace-api/src/main/java/org/dspace/discovery/DiscoverQuery.java b/dspace-api/src/main/java/org/dspace/discovery/DiscoverQuery.java index e133ad0ed170..185c768a057b 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/DiscoverQuery.java +++ b/dspace-api/src/main/java/org/dspace/discovery/DiscoverQuery.java @@ -72,6 +72,14 @@ public enum SORT_ORDER { private String discoveryConfigurationName; + /** + * The required authorizations user should have for the objects returned by the query. + * The READ authorization (Constants.READ) is always required and does not need to be added here. + */ + private List requiredAuthorization; + + private boolean inheritAuthorizations = true; + public DiscoverQuery() { //Initialize all our lists this.filterQueries = new ArrayList<>(); @@ -83,6 +91,7 @@ public DiscoverQuery() { this.hitHighlighting = new HashMap<>(); //Use a linked hashmap since sometimes insertion order might matter this.properties = new LinkedHashMap<>(); + this.requiredAuthorization = new ArrayList<>(); } @@ -411,4 +420,54 @@ public String getDiscoveryConfigurationName() { public void setDiscoveryConfigurationName(String discoveryConfigurationName) { this.discoveryConfigurationName = discoveryConfigurationName; } + + /** + * Return the required authorization user should have for the objects returned by this query + * + * @return the required authorizations + */ + public List getRequiredAuthorizations() { + return requiredAuthorization; + } + + /** + * Add a required authorization user should have for the objects returned by this query. + * The READ authorization (Constants.READ) is always required and does not need to be added here. + * + * @param action + * the required action + */ + public void addRequiredAuthorization(int action) { + this.requiredAuthorization.add(action); + } + + /** + * Remove a required authorization user should have for the objects returned by this query + * + * @param authorizationAction + * the required action + */ + public void removeRequiredAuthorization(int authorizationAction) { + this.requiredAuthorization.removeIf(action -> action == authorizationAction); + } + + /** + * Return whether authorizations should be inherited from parent objects + * + * @return true if authorizations should be inherited, false otherwise + */ + public boolean isInheritAuthorizationsEnabled() { + return inheritAuthorizations; + } + + /** + * Set whether authorizations should be inherited from parent objects + * + * @param inheritAuthorizations + * true if authorizations should be inherited, false otherwise + */ + public void setInheritAuthorizations(boolean inheritAuthorizations) { + this.inheritAuthorizations = inheritAuthorizations; + } + } diff --git a/dspace-api/src/main/java/org/dspace/discovery/SearchService.java b/dspace-api/src/main/java/org/dspace/discovery/SearchService.java index cb945648e7cd..7f4c85c3a864 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SearchService.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SearchService.java @@ -90,6 +90,8 @@ DiscoverFilterQuery toFilterQuery(Context context, String field, String operator List getRelatedItems(Context context, Item item, DiscoveryMoreLikeThisConfiguration moreLikeThisConfiguration); + String createLocationQueryForAdministrableDSOs(String epersonAndGroupClause); + /** * Method to create a Query that includes all * communities and collections a user may administrate. diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index 0c5c29cba3a3..c3b5cc78a243 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -597,6 +597,58 @@ protected boolean requiresIndexing(String uniqueId, Date lastModified) return reindexItem || !inIndex; } + /** + * Retrieves from Solr the list of administrable communities and collections for the + * current user based on a clause containing the e-person and group IDs. + * Builds and returns the "location" query part for these DSO's. + * + * @param epersonAndGroupClause A Solr filter clause containing one or more IDs combined with OR, + * e.g. {@code "eUUIDe1 OR gUUIDg2 OR gUUIDg3 OR ..."}. + * + * @return An empty string if no administrable DSO exists, or a string in the form + * {@code "location:(mUUID1 OR lUUID2 ... )"} when there are administrable DSO's. + */ + @Override + public String createLocationQueryForAdministrableDSOs(String epersonAndGroupClause) { + StringBuilder locationQuery = new StringBuilder(); + try { + + SolrQuery solrQuery = new SolrQuery(); + + String query = "*:*"; + solrQuery.setQuery(query); + solrQuery.addField(SearchUtils.RESOURCE_ID_FIELD); + solrQuery.addField(SearchUtils.RESOURCE_TYPE_FIELD); + solrQuery.addFilterQuery("(" + SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE + " OR " + + SearchUtils.RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE + ")"); + solrQuery.addFilterQuery("admin:(" + epersonAndGroupClause + ")"); + solrQuery.setRows(Integer.MAX_VALUE); + + QueryResponse solrQueryResponse = solrSearchCore.getSolr().query(solrQuery, + solrSearchCore.REQUEST_METHOD); + if (solrQueryResponse != null) { + List containerUUIDs = new ArrayList<>(); + for (SolrDocument doc : solrQueryResponse.getResults()) { + String type = (String) doc.getFieldValue(SearchUtils.RESOURCE_TYPE_FIELD); + String uniqueID = (String) doc.getFieldValue(SearchUtils.RESOURCE_ID_FIELD); + if (IndexableCommunity.TYPE.equals(type)) { + containerUUIDs.add("m" + uniqueID); + } else if (IndexableCollection.TYPE.equals(type)) { + containerUUIDs.add("l" + uniqueID); + } + } + if (!containerUUIDs.isEmpty()) { + locationQuery.append("location:("); + locationQuery.append(String.join(" OR ", containerUUIDs)); + return locationQuery.append(")").toString(); + } + } + } catch (Exception e) { + log.error("Failed to retrieve administrable communities and collections from Solr:", e); + } + return ""; + } + @Override public String createLocationQueryForAdministrableItems(Context context) throws SQLException { diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceResourceRestrictionPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceResourceRestrictionPlugin.java index d19616a85e10..c16317771a64 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceResourceRestrictionPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceResourceRestrictionPlugin.java @@ -18,12 +18,8 @@ import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; -import org.dspace.content.Collection; -import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.InProgressSubmission; -import org.dspace.content.Item; -import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; import org.dspace.core.Constants; @@ -36,14 +32,14 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; -import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; import org.dspace.xmlworkflow.storedcomponents.PoolTask; import org.springframework.beans.factory.annotation.Autowired; /** * Restriction plugin that ensures that indexes all the resource policies. - * When a search is performed extra filter queries are added to retrieve only results to which the user has READ access + * When a search is performed extra filter queries are added to retrieve only results to which the user has the + * required authorization. * * @author Kevin Van de Velde (kevin at atmire dot com) * @author Mark Diggory (markd at atmire dot com) @@ -64,6 +60,8 @@ public class SolrServiceResourceRestrictionPlugin implements SolrServiceIndexPlu protected GroupService groupService; @Autowired(required = true) protected ResourcePolicyService resourcePolicyService; + @Autowired + protected SearchService searchService; @Override public void additionalIndex(Context context, IndexableObject idxObj, SolrInputDocument document) { @@ -83,50 +81,32 @@ public void additionalIndex(Context context, IndexableObject idxObj, SolrInputDo } if (dso != null) { try { - List policies = authorizeService.getPoliciesActionFilter(context, dso, Constants.READ); - for (ResourcePolicy resourcePolicy : policies) { - if (resourcePolicyService.isDateValid(resourcePolicy)) { - String fieldValue; - if (resourcePolicy.getGroup() != null) { - //We have a group add it to the value - fieldValue = "g" + resourcePolicy.getGroup().getID(); - } else { - //We have an eperson add it to the value - fieldValue = "e" + resourcePolicy.getEPerson().getID(); - - } - - document.addField("read", fieldValue); - } - - //remove the policy from the cache to save memory - context.uncacheEntity(resourcePolicy); - } - // also index ADMIN policies as ADMIN permissions provides READ access - // going up through the hierarchy for communities, collections and items - while (dso != null) { - if (dso instanceof Community || dso instanceof Collection || dso instanceof Item) { - List policiesAdmin = authorizeService - .getPoliciesActionFilter(context, dso, Constants.ADMIN); - for (ResourcePolicy resourcePolicy : policiesAdmin) { - if (resourcePolicyService.isDateValid(resourcePolicy)) { - String fieldValue; - if (resourcePolicy.getGroup() != null) { - // We have a group add it to the value - fieldValue = "g" + resourcePolicy.getGroup().getID(); - } else { - // We have an eperson add it to the value - fieldValue = "e" + resourcePolicy.getEPerson().getID(); - } - document.addField("read", fieldValue); - document.addField("admin", fieldValue); + // Index read, submit, edit and admin permissions + int[] actionsToIndex = new int[] { Constants.READ, Constants.WRITE, Constants.ADD, Constants.ADMIN }; + + for (int action : actionsToIndex) { + String indexedActionName = getIndexedActionName(action); + List policies = authorizeService.getPoliciesActionFilter(context, dso, action); + for (ResourcePolicy resourcePolicy : policies) { + if (resourcePolicyService.isDateValid(resourcePolicy)) { + String fieldValue; + // Avoid NPE in cases where the policy does not have group or eperson + if (resourcePolicy.getGroup() == null && resourcePolicy.getEPerson() == null) { + continue; } - - // remove the policy from the cache to save memory - context.uncacheEntity(resourcePolicy); + if (resourcePolicy.getGroup() != null) { + //We have a group add it to the value + fieldValue = "g" + resourcePolicy.getGroup().getID(); + } else { + //We have an eperson add it to the value + fieldValue = "e" + resourcePolicy.getEPerson().getID(); + } + document.addField(indexedActionName, fieldValue); } + + //remove the policy from the cache to save memory + context.uncacheEntity(resourcePolicy); } - dso = ContentServiceFactory.getInstance().getDSpaceObjectService(dso).getParentObject(context, dso); } } catch (SQLException e) { log.error(LogHelper.getHeader(context, "Error while indexing resource policies", @@ -140,36 +120,66 @@ public void additionalIndex(Context context, IndexableObject idxObj, SolrInputDo public void additionalSearchParameters(Context context, DiscoverQuery discoveryQuery, SolrQuery solrQuery) { try { if (!authorizeService.isAdmin(context)) { - StringBuilder resourceQuery = new StringBuilder(); - //Always add the anonymous group id to the query - Group anonymousGroup = groupService.findByName(context, Group.ANONYMOUS); - String anonGroupId = ""; - if (anonymousGroup != null) { - anonGroupId = anonymousGroup.getID().toString(); - } - resourceQuery.append("read:(g" + anonGroupId); + EPerson currentUser = context.getCurrentUser(); + StringBuilder epersonAndGroupClause = new StringBuilder(); if (currentUser != null) { - resourceQuery.append(" OR e").append(currentUser.getID()); + epersonAndGroupClause.append("e").append(currentUser.getID()); } - //Retrieve all the groups the current user is a member of ! Set groups = groupService.allMemberGroupsSet(context, currentUser); for (Group group : groups) { - resourceQuery.append(" OR g").append(group.getID()); + if (!epersonAndGroupClause.isEmpty()) { + epersonAndGroupClause.append(" OR g").append(group.getID()); + } else { + epersonAndGroupClause.append("g").append(group.getID()); + } } - resourceQuery.append(")"); + StringBuilder resourceQuery = new StringBuilder(); - String locations = DSpaceServicesFactory.getInstance() - .getServiceManager() - .getServiceByName(SearchService.class.getName(), - SearchService.class) - .createLocationQueryForAdministrableItems(context); + List actions = discoveryQuery.getRequiredAuthorizations(); + /* + * The `actions` list specifies the permissions required beyond the default "read" permission. + * It should not include "read" because checking for "read" is always implicit. + * + * The query is constructed as follows: + * - If no actions are provided, it checks only for "read" or "admin" permissions. + * - If "admin" is in the `actions` list, it checks only for admin permissions. + * - Otherwise, it checks for both "read" and the other specified actions. + * + * The resulting query follows this structure: (read AND action) OR admin. + */ + if (actions.isEmpty()) { + // If no actions are included, we only check for read permissions + resourceQuery.append("(read:(").append(epersonAndGroupClause).append("))").append( " OR ") + .append("admin:(").append(epersonAndGroupClause).append(")"); + } else if (actions.contains(Constants.ADMIN)) { + // If the actions array contains the admin action, we only check for admin permissions + resourceQuery.append("admin:(").append(epersonAndGroupClause).append(")"); + } else { + // If the actions array contains other actions, we check for read permissions and the actions passed + resourceQuery.append("(read:(").append(epersonAndGroupClause).append(")"); + for (int action : actions) { + String actionName = getIndexedActionName(action); + resourceQuery.append(" AND ").append(actionName).append(":(").append(epersonAndGroupClause) + .append(")"); + } + resourceQuery.append(")"); + resourceQuery.append(" OR ").append("admin:(") + .append(epersonAndGroupClause).append(")"); + } - if (StringUtils.isNotBlank(locations)) { - resourceQuery.append(" OR "); - resourceQuery.append(locations); + // Add to the query the locations the user has administrative rights on to cover the cases of + // inherited permissions only if the inherit authorizations flag is enabled + if (discoveryQuery.isInheritAuthorizationsEnabled()) { + String locations = searchService + .createLocationQueryForAdministrableDSOs(epersonAndGroupClause.toString()); + + if (StringUtils.isNotBlank(locations)) { + resourceQuery.append(" OR "); + resourceQuery.append(locations); + } } solrQuery.addFilterQuery(resourceQuery.toString()); @@ -178,4 +188,26 @@ public void additionalSearchParameters(Context context, DiscoverQuery discoveryQ log.error(LogHelper.getHeader(context, "Error while adding resource policy information to query", ""), e); } } + + /** + * Get the action name used for solr indexing for the given action id + * + * @param action action id + * @return solr action name used for indexing + */ + private String getIndexedActionName(int action) { + + switch (action) { + case Constants.READ: + return "read"; + case Constants.WRITE: + return "edit"; + case Constants.ADD: + return "submit"; + case Constants.ADMIN: + return "admin"; + default: + return Constants.actionText[action].toLowerCase(); + } + } } diff --git a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java index 02154e715c55..f4c2b13b9943 100644 --- a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java @@ -685,8 +685,8 @@ public void testDeleteItemWithMultipleVersions() throws Exception { @Test public void testFindItemsWithEditNoRights() throws Exception { context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(0)); assertThat(count, equalTo(0)); } @@ -698,8 +698,8 @@ public void testFindAndCountItemsWithEditEPerson() throws Exception { .withAction(Constants.WRITE) .build(); context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(1)); assertThat(count, equalTo(1)); } @@ -711,8 +711,8 @@ public void testFindAndCountItemsWithAdminEPerson() throws Exception { .withAction(Constants.ADMIN) .build(); context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(1)); assertThat(count, equalTo(1)); } @@ -730,8 +730,8 @@ public void testFindAndCountItemsWithEditGroup() throws Exception { .withAction(Constants.WRITE) .build(); context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(1)); assertThat(count, equalTo(1)); } @@ -749,8 +749,8 @@ public void testFindAndCountItemsWithAdminGroup() throws Exception { .withAction(Constants.ADMIN) .build(); context.setCurrentUser(eperson); - List result = itemService.findItemsWithEdit(context, 0, 10); - int count = itemService.countItemsWithEdit(context); + List result = itemService.findItemsWithEdit(context, "", 0, 10); + int count = itemService.countItemsWithEdit(context, ""); assertThat(result.size(), equalTo(1)); assertThat(count, equalTo(1)); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java index 5c605daaf407..d938cd5a335e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditItemFeature.java @@ -40,7 +40,7 @@ public class EditItemFeature implements AuthorizationFeature { @Override public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException { if (object instanceof SiteRest) { - return itemService.countItemsWithEdit(context) > 0; + return itemService.countItemsWithEdit(context, "") > 0; } else if (object instanceof ItemRest) { Item item = (Item) utils.getDSpaceAPIObjectFromRest(context, object); return authService.authorizeActionBoolean(context, item, Constants.WRITE); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java index 3793928fb0fe..599bdb64117f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/SubmitFeature.java @@ -43,7 +43,7 @@ public class SubmitFeature implements AuthorizationFeature { public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException { if (object instanceof SiteRest) { // Check whether the user has permission to add to any collection - return collectionService.countCollectionsWithSubmit("", context, null) > 0; + return collectionService.countCollectionsWithSubmit(context, "", null) > 0; } else if (object instanceof CollectionRest) { // Check whether the user has permission to add to the given collection Collection collection = (Collection) utils.getDSpaceAPIObjectFromRest(context, object); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java index 0d59aeb254b5..5e28bdb30645 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java @@ -185,7 +185,7 @@ public Page findSubmitAuthorizedByCommunity( List collections = cs.findCollectionsWithSubmit(q, context, com, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - int tot = cs.countCollectionsWithSubmit(q, context, com); + int tot = cs.countCollectionsWithSubmit(context, q, com); return converter.toRestPage(collections, pageable, tot , utils.obtainProjection()); } catch (SQLException | SearchServiceException e) { throw new RuntimeException(e.getMessage(), e); @@ -200,7 +200,7 @@ public Page findSubmitAuthorized(@Parameter(value = "query") Str List collections = cs.findCollectionsWithSubmit(q, context, null, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - int tot = cs.countCollectionsWithSubmit(q, context, null); + int tot = cs.countCollectionsWithSubmit(context, q, null); return converter.toRestPage(collections, pageable, tot, utils.obtainProjection()); } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); @@ -211,14 +211,33 @@ public Page findSubmitAuthorized(@Parameter(value = "query") Str @SearchRestMethod(name = "findAdminAuthorized") public Page findAdminAuthorized ( Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.ADMIN, query); + } + + /** + * Returns Collections for which the current user has 'edit' privileges. + * + * @param pageable The pagination information + * @param query The query used in the lookup + * @return + */ + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "findEditAuthorized") + public Page findEditAuthorized ( + Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.WRITE, query); + } + + private Page findAuthorized(Pageable pageable, int action, String query) { try { Context context = obtainContext(); - List collections = authorizeService.findAdminAuthorizedCollection(context, query, + List collections = authorizeService.findAuthorizedCollectionByAction(context, query, + action, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - long tot = authorizeService.countAdminAuthorizedCollection(context, query); - return converter.toRestPage(collections, pageable, tot , utils.obtainProjection()); - } catch (SearchServiceException | SQLException e) { + long tot = authorizeService.countAuthorizedCollectionByAction(context, query, action); + return converter.toRestPage(collections, pageable, tot, utils.obtainProjection()); + } catch (SearchServiceException e) { throw new RuntimeException(e.getMessage(), e); } } @@ -245,10 +264,10 @@ public Page findSubmitAuthorizedByEntityType( if (entityType == null) { throw new ResourceNotFoundException("There was no entityType found with label: " + entityTypeLabel); } - List collections = cs.findCollectionsWithSubmit(query, context, null, entityTypeLabel, + List collections = cs.findCollectionsWithSubmit(context, query,null, entityTypeLabel, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - int tot = cs.countCollectionsWithSubmit(query, context, null, entityTypeLabel); + int tot = cs.countCollectionsWithSubmit(context, query,null, entityTypeLabel); return converter.toRestPage(collections, pageable, tot, utils.obtainProjection()); } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); @@ -282,10 +301,10 @@ public Page findSubmitAuthorizedByCommunityAndEntityType( throw new ResourceNotFoundException( CommunityRest.CATEGORY + "." + CommunityRest.NAME + " with id: " + communityUuid + " not found"); } - List collections = cs.findCollectionsWithSubmit(query, context, community, entityTypeLabel, + List collections = cs.findCollectionsWithSubmit(context, query, community, entityTypeLabel, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - int total = cs.countCollectionsWithSubmit(query, context, community, entityTypeLabel); + int total = cs.countCollectionsWithSubmit(context, query, community, entityTypeLabel); return converter.toRestPage(collections, pageable, total, utils.obtainProjection()); } catch (SQLException | SearchServiceException e) { throw new RuntimeException(e.getMessage(), e); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CommunityRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CommunityRestRepository.java index 0d4e6be133e8..9bbdd3c75d6e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CommunityRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/CommunityRestRepository.java @@ -38,6 +38,7 @@ import org.dspace.content.Community; import org.dspace.content.service.BitstreamService; import org.dspace.content.service.CommunityService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.DiscoverResult; @@ -220,12 +221,31 @@ public Page findAllTop(Pageable pageable) { @SearchRestMethod(name = "findAdminAuthorized") public Page findAdminAuthorized ( Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.ADMIN, query); + } + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "findEditAuthorized") + public Page findEditAuthorized ( + Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.WRITE, query); + } + + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "findAddAuthorized") + public Page findAddAuthorized ( + Pageable pageable, @Parameter(value = "query") String query) { + return findAuthorized(pageable, Constants.ADD, query); + } + + private Page findAuthorized(Pageable pageable, int action, String query) { try { Context context = obtainContext(); - List communities = authorizeService.findAdminAuthorizedCommunity(context, query, + List communities = authorizeService.findAuthorizedCommunityByAction(context, query, + action, Math.toIntExact(pageable.getOffset()), Math.toIntExact(pageable.getPageSize())); - long tot = authorizeService.countAdminAuthorizedCommunity(context, query); + long tot = authorizeService.countAuthorizedCommunityByAction(context, query, action); return converter.toRestPage(communities, pageable, tot , utils.obtainProjection()); } catch (SearchServiceException | SQLException e) { throw new RuntimeException(e.getMessage(), e); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java index 1a22a5f7477f..c4a800681bba 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java @@ -22,6 +22,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.converter.MetadataConverter; import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; @@ -45,6 +47,7 @@ import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -357,6 +360,27 @@ public Bundle addBundleToItem(Context context, Item item, BundleRest bundleRest) return bundle; } + /** + * Method to find the items for which the current user has editing rights. + * + * @param query Query string + * @param pageable Pagination information + * @return Page of Items (REST representation) for which the current user has editing rights + * @throws SearchServiceException + */ + @PreAuthorize("hasAuthority('AUTHENTICATED')") + @SearchRestMethod(name = "findEditAuthorized") + public Page findEditAuthorized(@Parameter(value = "query") String query, + Pageable pageable) + throws SearchServiceException { + Context context = obtainContext(); + List items = itemService.findItemsWithEdit(context, query, + Math.toIntExact(pageable.getOffset()), + Math.toIntExact(pageable.getPageSize())); + int tot = itemService.countItemsWithEdit(context, query); + return converter.toRestPage(items, pageable, tot, utils.obtainProjection()); + } + @Override protected ItemRest createAndReturn(Context context, List stringList) throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException { diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 92917818132e..8a0fbd0dd78f 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -30,8 +30,6 @@ - - From fe5c76fd3ab14f26531d0a0c4beb02c9d6c9be2f Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Sat, 29 Nov 2025 22:29:51 +0100 Subject: [PATCH 538/701] Introduce integration tests for the new authorization-based endpoints for Item, Collection, and Community --- .../app/rest/CollectionRestRepositoryIT.java | 362 +++++++++++++ .../app/rest/CommunityRestRepositoryIT.java | 481 ++++++++++++++++++ .../dspace/app/rest/ItemRestRepositoryIT.java | 408 +++++++++++++++ 3 files changed, 1251 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java index 7a2f049b4e1d..1a66a15b6f2e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java @@ -56,6 +56,7 @@ import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.builder.CollectionBuilder; @@ -3315,6 +3316,109 @@ public void addColAdminGroupToCheckReindexingTest() throws Exception { .andExpect(jsonPath("$.page.totalElements", is(1))); } + @Test + public void addParentComAdminGroupToCheckAdminPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findAdminAuthorized"); + } + + @Test + public void addParentComAdminGroupToCheckEditPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findEditAuthorized"); + } + + public void addParentComAdminGroupToCheckGenericPropagationTest(String method) throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("MyTest") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/collections/search/" + method) + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + AtomicReference idRef = new AtomicReference<>(); + ObjectMapper mapper = new ObjectMapper(); + GroupRest groupRest = new GroupRest(); + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/core/communities/" + parentCommunity.getID() + "/adminGroup") + .content(mapper.writeValueAsBytes(groupRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set( + UUID.fromString(read(result.getResponse().getContentAsString(), "$.id"))) + ); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(post("/api/eperson/groups/" + idRef.get() + "/epersons") + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content(REST_SERVER_URL + "eperson/groups/" + eperson.getID() + )); + + getClient(epersonToken).perform(get("/api/core/collections/search/" + method) + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains(CollectionMatcher + .matchProperties(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void removeParentComAdminPolicyToCheckAdminPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findAdminAuthorized"); + } + + @Test + public void removeParentComAdminPolicyToCheckEditPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findEditAuthorized"); + } + + public void removeParentComAdminPolicyToCheckGenericPropagationTest(String method) throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, eperson, null) + .withDspaceObject(parentCommunity).withAction(Constants.ADMIN) + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("MyTest") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/collections/search/" + method) + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains(CollectionMatcher + .matchProperties(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(delete("/api/authz/resourcepolicies/" + policy.getID())) + .andExpect(status().is(204)); + + getClient(epersonToken).perform(get("/api/core/collections/search/" + method) + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + @Test public void findAuthorizedCollectionsByEntityType() throws Exception { context.turnOffAuthorisationSystem(); @@ -3653,4 +3757,262 @@ public void findSubmitAuthorizedByCommunityAndEntityTypeNotFoundTest() throws Ex .andExpect(status().isNotFound()); } + @Test + public void findEditAuthorizedUnauthorizedTest() throws Exception { + getClient().perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findEditAuthorizedResourcePolicyTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community comm1 = CommunityBuilder.createCommunity(context).withName("Community 1").build(); + + EPerson hasDirectEditRights = EPersonBuilder.createEPerson(context) + .withEmail("has@editrights.com").withPassword(password) + .build(); + EPerson hasDirectAdminRights = EPersonBuilder.createEPerson(context) + .withEmail("has@adminrights.com").withPassword(password) + .build(); + Collection byResourcePolicy = CollectionBuilder.createCollection(context, comm1) + .withName("direct edit rights for eperson") + .build(); + Collection uneditable = CollectionBuilder.createCollection(context, comm1) + .withName("uneditable collection") + .build(); + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectEditRights, null) + .withDspaceObject(byResourcePolicy).withAction(WRITE) + .build(); + policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectAdminRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADMIN) + .build(); + context.restoreAuthSystemState(); + + String tokenHasDirectEditRightsToken = getAuthToken(hasDirectEditRights.getEmail(), password); + String tokenHasDirectAdminRightsToken = getAuthToken(hasDirectAdminRights.getEmail(), password); + + getClient(tokenHasDirectEditRightsToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.contains(CollectionMatcher.matchCollection(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(tokenHasDirectAdminRightsToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.contains(CollectionMatcher.matchCollection(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedAdminPropagationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + /* + DSO structure: + root + ├── subcomm1 + ├── subcomm1collA (collection) + └── subcomm2subcomm3 (community) + ├── subcomm2subcomm3collB (collection) + └── subcomm2 + └── subcomm2coll + */ + EPerson rootAdmin = EPersonBuilder.createEPerson(context) + .withEmail("root@admin.com").withPassword(password).build(); + EPerson subcomm1Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1@admin.com").withPassword(password).build(); + EPerson subcomm2Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2@admin.com").withPassword(password).build(); + EPerson subcomm1collA_Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1collA@admin.com").withPassword(password).build(); + EPerson subcomm1collB_Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1collB@admin.com").withPassword(password).build(); + EPerson subcomm2collAdmin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2coll@admin.com").withPassword(password).build(); + + Community root = CommunityBuilder.createCommunity(context) + .withAdminGroup(rootAdmin) + .withName("root") + .build(); + Community subcomm1 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm1Admin) + .withName("subcomm1") + .build(); + Community subcomm1subcom3 = CommunityBuilder.createSubCommunity(context, subcomm1) + .withName("subcomm1subcom3") + .build(); + Community subcomm2 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm2Admin) + .withName("subcomm2") + .build(); + Collection subcomm1collA = CollectionBuilder.createCollection(context, subcomm1) + .withAdminGroup(subcomm1collA_Admin) + .withName("subcomm1collA") + .build(); + Collection subcomm2subcomm3collB = CollectionBuilder.createCollection(context, subcomm1subcom3) + .withAdminGroup(subcomm1collB_Admin) + .withName("subcomm2subcomm3collB") + .build(); + Collection subcomm2coll = CollectionBuilder.createCollection(context, subcomm2) + .withAdminGroup(subcomm2collAdmin) + .withName("subcomm2coll") + .build(); + context.restoreAuthSystemState(); + + String siteAdminToken = getAuthToken(admin.getEmail(), password); + String rootAdminToken = getAuthToken(rootAdmin.getEmail(), password); + String subcomm1AdminToken = getAuthToken(subcomm1Admin.getEmail(), password); + String subcomm2AdminToken = getAuthToken(subcomm2Admin.getEmail(), password); + + getClient(siteAdminToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.containsInAnyOrder( + CollectionMatcher.matchCollection(subcomm1collA), + CollectionMatcher.matchCollection(subcomm2subcomm3collB), + CollectionMatcher.matchCollection(subcomm2coll) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(rootAdminToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.containsInAnyOrder( + CollectionMatcher.matchCollection(subcomm1collA), + CollectionMatcher.matchCollection(subcomm2subcomm3collB), + CollectionMatcher.matchCollection(subcomm2coll) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(subcomm1AdminToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.containsInAnyOrder( + CollectionMatcher.matchCollection(subcomm1collA), + CollectionMatcher.matchCollection(subcomm2subcomm3collB) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(subcomm2AdminToken).perform(get("/api/core/collections/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.collections", + Matchers.containsInAnyOrder( + CollectionMatcher.matchCollection(subcomm2coll) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedCollectionsWithQueryTest() throws Exception { + findGenericAuthorizedCollectionsWithQueryTest("findEditAuthorized"); + } + + @Test + public void findReadAuthorizedCollectionsWithQueryTest() throws Exception { + findGenericAuthorizedCollectionsWithQueryTest("findAdminAuthorized"); + } + + public void findGenericAuthorizedCollectionsWithQueryTest(String method) throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson eperson2 = EPersonBuilder.createEPerson(context) + .withEmail("eperson2@mail.com") + .withPassword(password) + .build(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Community child2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community Two") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Sample collection") + .withAdminGroup(eperson) + .build(); + Collection col2 = CollectionBuilder.createCollection(context, child1) + .withName("Test collection") + .build(); + Collection col3 = CollectionBuilder.createCollection(context, child2) + .withName("Collection of sample items") + .withAdminGroup(eperson) + .build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + // Test simple query matches + getClient(tokenEPerson).perform(get("/api/core/collections/search/" + method) + .param("query", "collection")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col1.getName(), col1.getID(), col1.getHandle()), + CollectionMatcher.matchProperties(col3.getName(), col3.getID(), col3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test insensitive matches + getClient(tokenEPerson).perform(get("/api/core/collections/search/" + method) + .param("query", "COLLECTION")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col1.getName(), col1.getID(), col1.getHandle()), + CollectionMatcher.matchProperties(col3.getName(), col3.getID(), col3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test collection unathorized for eperson is not returned + getClient(tokenEPerson).perform(get("/api/core/collections/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test eperson with no authorized collections + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + getClient(tokenEPerson2).perform(get("/api/core/collections/search/" + method) + .param("query", "collection")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test admin gets all authorized collections + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/collections/search/" + method) + .param("query", "sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col1.getName(), col1.getID(), col1.getHandle()), + CollectionMatcher.matchProperties(col3.getName(), col3.getID(), col3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test query with unsorted query words + getClient(tokenAdmin).perform(get("/api/core/collections/search/" + method) + .param("query", "items sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains( + CollectionMatcher.matchProperties(col3.getName(), col3.getID(), col3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test collection not authorized for eperson is returned for admin + getClient(tokenAdmin).perform(get("/api/core/collections/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col2.getName(), col2.getID(), col2.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java index 6837b8900398..6e446e6de0a2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java @@ -50,6 +50,7 @@ import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.builder.CollectionBuilder; @@ -2763,4 +2764,484 @@ public void addComAdminGroupToCheckReindexingTest() throws Exception { .andExpect(jsonPath("$.page.totalElements", is(1))); } + @Test + public void addParentComAdminGroupToCheckAdminPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findAdminAuthorized"); + } + + @Test + public void addParentComAdminGroupToCheckEditPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findEditAuthorized"); + } + + @Test + public void addParentComAdminGroupToCheckAddPropagationTest() throws Exception { + addParentComAdminGroupToCheckGenericPropagationTest("findAddAuthorized"); + } + + public void addParentComAdminGroupToCheckGenericPropagationTest(String method) throws Exception { + context.turnOffAuthorisationSystem(); + + Community rootCommunity = CommunityBuilder.createCommunity(context) + .withName("Root Community") + .build(); + + Community subCommunity = CommunityBuilder.createSubCommunity(context, rootCommunity) + .withName("MyTestCom") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/communities/search/" + method) + .param("query", "MyTestCom")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + AtomicReference idRef = new AtomicReference<>(); + ObjectMapper mapper = new ObjectMapper(); + GroupRest groupRest = new GroupRest(); + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/core/communities/" + rootCommunity.getID() + "/adminGroup") + .content(mapper.writeValueAsBytes(groupRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set( + UUID.fromString(read(result.getResponse().getContentAsString(), "$.id"))) + ); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(post("/api/eperson/groups/" + idRef.get() + "/epersons") + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content(REST_SERVER_URL + "eperson/groups/" + eperson.getID() + )); + + getClient(epersonToken).perform(get("/api/core/communities/search/" + method) + .param("query", "MyTestCom")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains(CommunityMatcher + .matchProperties(subCommunity.getName(), + subCommunity.getID(), + subCommunity.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void removeParentComAdminPolicyToCheckAdminPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findAdminAuthorized"); + } + + @Test + public void removeParentComAdminPolicyToCheckEditPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findEditAuthorized"); + } + + @Test + public void removeParentComAdminPolicyToCheckAddPropagationTest() throws Exception { + removeParentComAdminPolicyToCheckGenericPropagationTest("findAddAuthorized"); + } + + public void removeParentComAdminPolicyToCheckGenericPropagationTest(String method) throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Root Community") + .build(); + + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, eperson, null) + .withDspaceObject(parentCommunity).withAction(Constants.ADMIN) + .build(); + + Community subCommunity = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("MyTestCom") + .build(); + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/communities/search/" + method) + .param("query", "MyTestCom")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains(CommunityMatcher + .matchProperties(subCommunity.getName(), + subCommunity.getID(), + subCommunity.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(delete("/api/authz/resourcepolicies/" + policy.getID())) + .andExpect(status().is(204)); + + getClient(epersonToken).perform(get("/api/core/communities/search/" + method) + .param("query", "MyTestCom")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + @Test + public void findEditAuthorizedUnauthorizedTest() throws Exception { + getClient().perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findEditAuthorizedResourcePolicyTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community comm1 = CommunityBuilder.createCommunity(context).withName("Community 1").build(); + + EPerson hasDirectEditRights = EPersonBuilder.createEPerson(context) + .withEmail("has@editrights.com").withPassword(password) + .build(); + EPerson hasDirectAdminRights = EPersonBuilder.createEPerson(context) + .withEmail("has@adminrights.com").withPassword(password) + .build(); + Community byResourcePolicy = CommunityBuilder.createCommunity(context) + .withName("direct edit rights for eperson").build(); + Community uneditable = CommunityBuilder.createCommunity(context) + .withName("uneditable community") + .build(); + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectEditRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.WRITE) + .build(); + policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectAdminRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADMIN) + .build(); + context.restoreAuthSystemState(); + + String tokenHasDirectEditRightsToken = getAuthToken(hasDirectEditRights.getEmail(), password); + String tokenHasDirectAdminRightsToken = getAuthToken(hasDirectAdminRights.getEmail(), password); + + getClient(tokenHasDirectEditRightsToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.contains(CommunityMatcher.matchCommunity(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(tokenHasDirectAdminRightsToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.contains(CommunityMatcher.matchCommunity(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedAdminPropagationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + /* + DSO structure: + root + └── subcomm1 + └── subcomm1subcomm2 + */ + EPerson rootAdmin = EPersonBuilder.createEPerson(context) + .withEmail("root@admin.com").withPassword(password).build(); + EPerson subcomm1Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1@admin.com").withPassword(password).build(); + EPerson subcomm2Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2@admin.com").withPassword(password).build(); + + Community root = CommunityBuilder.createCommunity(context) + .withAdminGroup(rootAdmin) + .withName("root") + .build(); + Community subcomm1 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm1Admin) + .withName("subcomm1") + .build(); + Community subcomm1subcomm2 = CommunityBuilder.createSubCommunity(context, subcomm1) + .withAdminGroup(subcomm2Admin) + .withName("subcomm1subcomm2") + .build(); + context.restoreAuthSystemState(); + + String siteAdminToken = getAuthToken(admin.getEmail(), password); + String rootAdminToken = getAuthToken(rootAdmin.getEmail(), password); + String subcomm1AdminToken = getAuthToken(subcomm1Admin.getEmail(), password); + String subcomm2AdminToken = getAuthToken(subcomm2Admin.getEmail(), password); + + getClient(siteAdminToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(root), + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm1subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(rootAdminToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(root), + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm1subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(subcomm1AdminToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm1subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(subcomm2AdminToken).perform(get("/api/core/communities/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(subcomm1subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + } + + @Test + public void findAddAuthorizedUnauthorizedTest() throws Exception { + getClient().perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findAddAuthorizedResourcePolicyTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community comm1 = CommunityBuilder.createCommunity(context).withName("Community 1").build(); + + EPerson hasDirectEditRights = EPersonBuilder.createEPerson(context) + .withEmail("has@editrights.com").withPassword(password) + .build(); + EPerson hasDirectAdminRights = EPersonBuilder.createEPerson(context) + .withEmail("has@adminrights.com").withPassword(password) + .build(); + Community byResourcePolicy = CommunityBuilder.createCommunity(context) + .withName("direct add rights for eperson").build(); + Community uneditable = CommunityBuilder.createCommunity(context) + .withName("no add community") + .build(); + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectEditRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADD) + .build(); + policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectAdminRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADMIN) + .build(); + context.restoreAuthSystemState(); + + String tokenHasDirectAddRightsToken = getAuthToken(hasDirectEditRights.getEmail(), password); + String tokenHasDirectAdminRightsToken = getAuthToken(hasDirectAdminRights.getEmail(), password); + + getClient(tokenHasDirectAddRightsToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.contains(CommunityMatcher.matchCommunity(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(tokenHasDirectAdminRightsToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.contains(CommunityMatcher.matchCommunity(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findAddAuthorizedAdminPropagationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + /* + DSO structure: + root + └── subcomm1 + └── subcomm2 + */ + EPerson rootAdmin = EPersonBuilder.createEPerson(context) + .withEmail("root@admin.com").withPassword(password).build(); + EPerson subcomm1Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1@admin.com").withPassword(password).build(); + EPerson subcomm2Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2@admin.com").withPassword(password).build(); + + Community root = CommunityBuilder.createCommunity(context) + .withAdminGroup(rootAdmin) + .withName("root") + .build(); + Community subcomm1 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm1Admin) + .withName("subcomm1") + .build(); + Community subcomm2 = CommunityBuilder.createSubCommunity(context, subcomm1) + .withAdminGroup(subcomm2Admin) + .withName("subcomm2") + .build(); + context.restoreAuthSystemState(); + + String siteAdminToken = getAuthToken(admin.getEmail(), password); + String rootAdminToken = getAuthToken(rootAdmin.getEmail(), password); + String subcomm1AdminToken = getAuthToken(subcomm1Admin.getEmail(), password); + String subcomm2AdminToken = getAuthToken(subcomm2Admin.getEmail(), password); + + getClient(siteAdminToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(root), + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(rootAdminToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(root), + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(subcomm1AdminToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(subcomm1), + CommunityMatcher.matchCommunity(subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(subcomm2AdminToken).perform(get("/api/core/communities/search/findAddAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.communities", + Matchers.containsInAnyOrder( + CommunityMatcher.matchCommunity(subcomm2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedCommunitiesWithQueryTest() throws Exception { + findGenericAuthorizedCommunitiesWithQueryTest("findEditAuthorized"); + } + + @Test + public void findAddAuthorizedCommunitiesWithQueryTest() throws Exception { + findGenericAuthorizedCommunitiesWithQueryTest("findAddAuthorized"); + } + + @Test + public void findReadAuthorizedCommunitiesWithQueryTest() throws Exception { + findGenericAuthorizedCommunitiesWithQueryTest("findAdminAuthorized"); + } + + public void findGenericAuthorizedCommunitiesWithQueryTest(String method) throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson eperson2 = EPersonBuilder.createEPerson(context) + .withEmail("eperson2@mail.com") + .withPassword(password) + .build(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Community child2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community Two") + .build(); + Community com1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sample community") + .withAdminGroup(eperson) + .build(); + Community com2 = CommunityBuilder.createSubCommunity(context, child1) + .withName("Test community") + .build(); + Community com3 = CommunityBuilder.createSubCommunity(context, child2) + .withName("community of sample items") + .withAdminGroup(eperson) + .build(); + context.restoreAuthSystemState(); + + // Test simple query + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/communities/search/" + method) + .param("query", "community")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com1.getName(), com1.getID(), com1.getHandle()), + CommunityMatcher.matchProperties(com3.getName(), com3.getID(), com3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test case insensitive query + getClient(tokenEPerson).perform(get("/api/core/communities/search/" + method) + .param("query", "COMMUNITY")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com1.getName(), com1.getID(), com1.getHandle()), + CommunityMatcher.matchProperties(com3.getName(), com3.getID(), com3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test word for unauthorized community + getClient(tokenEPerson).perform(get("/api/core/communities/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test eperson with no authorized communities + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + getClient(tokenEPerson2).perform(get("/api/core/communities/search/" + method) + .param("query", "community")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test as admin + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/communities/search/" + method) + .param("query", "sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com1.getName(), com1.getID(), com1.getHandle()), + CommunityMatcher.matchProperties(com3.getName(), com3.getID(), com3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test more specific unsorted query words + getClient(tokenAdmin).perform(get("/api/core/communities/search/" + method) + .param("query", "items sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains( + CommunityMatcher.matchProperties(com3.getName(), com3.getID(), com3.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test add retrieve the col not authorized to community admin user + getClient(tokenAdmin).perform(get("/api/core/communities/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com2.getName(), com2.getID(), com2.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index f54fdc38b60e..38e7fd9a2b54 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -26,6 +26,8 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE; +import static org.springframework.http.MediaType.parseMediaType; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; @@ -55,6 +57,7 @@ import org.dspace.app.rest.matcher.CollectionMatcher; import org.dspace.app.rest.matcher.HalMatcher; import org.dspace.app.rest.matcher.ItemMatcher; +import org.dspace.app.rest.model.GroupRest; import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.MetadataRest; import org.dspace.app.rest.model.MetadataValueRest; @@ -64,6 +67,7 @@ import org.dspace.app.rest.repository.ItemRestRepository; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.ResourcePolicy; import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.BundleBuilder; import org.dspace.builder.CollectionBuilder; @@ -4758,4 +4762,408 @@ public void findAccessStatusForItemTest() throws Exception { .andExpect(jsonPath("$.status", notNullValue())); } + @Test + public void findEditAuthorizedUnauthorizedTest() throws Exception { + getClient().perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findEditAuthorizedResourcePolicyTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community comm1 = CommunityBuilder.createCommunity(context).withName("Community 1").build(); + Collection col1 = CollectionBuilder.createCollection(context, comm1).withName("Collection 1").build(); + + EPerson hasDirectEditRights = EPersonBuilder.createEPerson(context) + .withEmail("has@editrights.com").withPassword(password) + .build(); + EPerson hasDirectAdminRights = EPersonBuilder.createEPerson(context) + .withEmail("has@adminrights.com").withPassword(password) + .build(); + Item byResourcePolicy = ItemBuilder.createItem(context, col1) + .withTitle("direct edit rights for eperson") + .build(); + Item uneditable = ItemBuilder.createItem(context, col1) + .withTitle("uneditable item") + .build(); + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectEditRights, null) + .withDspaceObject(byResourcePolicy).withAction(WRITE) + .build(); + policy = ResourcePolicyBuilder.createResourcePolicy(context, hasDirectAdminRights, null) + .withDspaceObject(byResourcePolicy).withAction(Constants.ADMIN) + .build(); + context.restoreAuthSystemState(); + + String tokenHasDirectEditRightsToken = getAuthToken(hasDirectEditRights.getEmail(), password); + String tokenHasDirectAdminRightsToken = getAuthToken(hasDirectAdminRights.getEmail(), password); + + getClient(tokenHasDirectEditRightsToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.contains(ItemMatcher.matchItemProperties(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(tokenHasDirectAdminRightsToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.contains(ItemMatcher.matchItemProperties(byResourcePolicy)))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findEditAuthorizedAdminPropagationTest() throws Exception { + /* + Cases: + - items in collection with admin rights + - items in collection in community with admin rights + */ + + context.turnOffAuthorisationSystem(); + + /* + DSO structure: + root + ├── subcomm1 + ├── subcomm1collA + ├── subcomm1collAitemX + ├── subcomm1collAitemY + ├── subcomm1collB + └── subcomm1collBitem + └── subcomm2 + └── subcomm2coll + └── subcomm2collitem + */ + EPerson rootAdmin = EPersonBuilder.createEPerson(context) + .withEmail("root@admin.com").withPassword(password).build(); + EPerson subcomm1Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1@admin.com").withPassword(password).build(); + EPerson subcomm2Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2@admin.com").withPassword(password).build(); + EPerson subcomm1collA_Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1collA@admin.com").withPassword(password).build(); + EPerson subcomm1collB_Admin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm1collB@admin.com").withPassword(password).build(); + EPerson subcomm2collAdmin = EPersonBuilder.createEPerson(context) + .withEmail("subcomm2coll@admin.com").withPassword(password).build(); + + Community root = CommunityBuilder.createCommunity(context) + .withAdminGroup(rootAdmin) + .withName("root") + .build(); + Community subcomm1 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm1Admin) + .withName("subcomm1") + .build(); + Community subcomm2 = CommunityBuilder.createSubCommunity(context, root) + .withAdminGroup(subcomm2Admin) + .withName("subcomm2") + .build(); + Collection subcomm1collA = CollectionBuilder.createCollection(context, subcomm1) + .withAdminGroup(subcomm1collA_Admin) + .withName("subcomm1collA") + .build(); + Collection subcomm1collB = CollectionBuilder.createCollection(context, subcomm1) + .withAdminGroup(subcomm1collB_Admin) + .withName("subcomm1collB") + .build(); + Collection subcomm2coll = CollectionBuilder.createCollection(context, subcomm2) + .withAdminGroup(subcomm2collAdmin) + .withName("subcomm2coll") + .build(); + Item subcomm1collAitemX = ItemBuilder.createItem(context, subcomm1collA).withTitle("subcomm1collAitemX") + .build(); + Item subcomm1collAitemY = ItemBuilder.createItem(context, subcomm1collA).withTitle("subcomm1collAitemY") + .build(); + Item subcomm1collBitem = ItemBuilder.createItem(context, subcomm1collB).withTitle("subcomm1collBitem") + .build(); + Item subcomm2collitem = ItemBuilder.createItem(context, subcomm2coll).withTitle("subcomm2collitem") + .build(); + context.restoreAuthSystemState(); + + String siteAdminToken = getAuthToken(admin.getEmail(), password); + String rootAdminToken = getAuthToken(rootAdmin.getEmail(), password); + String subcomm1AdminToken = getAuthToken(subcomm1Admin.getEmail(), password); + String subcomm2AdminToken = getAuthToken(subcomm2Admin.getEmail(), password); + String subcomm1collA_AdminToken = getAuthToken(subcomm1collA_Admin.getEmail(), password); + String subcomm1collB_AdminToken = getAuthToken(subcomm1collB_Admin.getEmail(), password); + String subcomm2collAdminToken = getAuthToken(subcomm2collAdmin.getEmail(), password); + + getClient(siteAdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collAitemX), + ItemMatcher.matchItemProperties(subcomm1collAitemY), + ItemMatcher.matchItemProperties(subcomm1collBitem), + ItemMatcher.matchItemProperties(subcomm2collitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(4))); + + getClient(rootAdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collAitemX), + ItemMatcher.matchItemProperties(subcomm1collAitemY), + ItemMatcher.matchItemProperties(subcomm1collBitem), + ItemMatcher.matchItemProperties(subcomm2collitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(4))); + + getClient(subcomm1AdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collAitemX), + ItemMatcher.matchItemProperties(subcomm1collAitemY), + ItemMatcher.matchItemProperties(subcomm1collBitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + getClient(subcomm2AdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm2collitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + getClient(subcomm1collA_AdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collAitemX), + ItemMatcher.matchItemProperties(subcomm1collAitemY) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(subcomm1collB_AdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm1collBitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + getClient(subcomm2collAdminToken).perform(get("/api/core/items/search/findEditAuthorized")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.items", + Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(subcomm2collitem) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void addParentComAdminGroupToCheckReindexingTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("col1") + .build(); + + Item item = ItemBuilder.createItem(context, col1) + .withTitle("MyTest") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/items/search/findEditAuthorized") + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + AtomicReference idRef = new AtomicReference<>(); + ObjectMapper mapper = new ObjectMapper(); + GroupRest groupRest = new GroupRest(); + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(post("/api/core/communities/" + parentCommunity.getID() + "/adminGroup") + .content(mapper.writeValueAsBytes(groupRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set( + UUID.fromString(read(result.getResponse().getContentAsString(), "$.id"))) + ); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(post("/api/eperson/groups/" + idRef.get() + "/epersons") + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content(REST_SERVER_URL + "eperson/groups/" + eperson.getID() + )); + + getClient(epersonToken).perform(get("/api/core/items/search/findEditAuthorized") + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains(ItemMatcher + .matchItemProperties(item) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void removeParentComAdminPolicyToCheckEditPropagationTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + ResourcePolicy policy = ResourcePolicyBuilder.createResourcePolicy(context, eperson, null) + .withDspaceObject(parentCommunity).withAction(Constants.ADMIN) + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("col1") + .build(); + + Item item = ItemBuilder.createItem(context, col1) + .withTitle("MyTest") + .build(); + + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/core/items/search/findEditAuthorized") + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains(ItemMatcher + .matchItemProperties(item) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + String token = getAuthToken(admin.getEmail(), password); + getClient(token).perform(delete("/api/authz/resourcepolicies/" + policy.getID())) + .andExpect(status().is(204)); + + getClient(epersonToken).perform(get("/api/core/items/search/findEditAuthorized") + .param("query", "MyTest")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + @Test + public void findEditAuthorizedItemsWithQueryTest() throws Exception { + findGenericAuthorizedItemsWithQueryTest("findEditAuthorized"); + } + + public void findGenericAuthorizedItemsWithQueryTest(String method) throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson eperson2 = EPersonBuilder.createEPerson(context) + .withEmail("eperson2@mail.com") + .withPassword(password) + .build(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Community child2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community Two") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Sample collection") + .withAdminGroup(eperson) + .build(); + Collection col2 = CollectionBuilder.createCollection(context, child1) + .withName("col2") + .build(); + Collection col3 = CollectionBuilder.createCollection(context, child2) + .withName("col3") + .withAdminGroup(eperson) + .build(); + Item item1 = ItemBuilder.createItem(context, col1) + .withTitle("Sample item") + .build(); + Item item2 = ItemBuilder.createItem(context, col2) + .withTitle("Test item") + .build(); + Item item3 = ItemBuilder.createItem(context, col3) + .withTitle("Item of sample bitstreams") + .build(); + + context.restoreAuthSystemState(); + + // Test simple query + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/items/search/" + method) + .param("query", "item")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item1), + ItemMatcher.matchItemProperties(item3) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test case insensitive + getClient(tokenEPerson).perform(get("/api/core/items/search/" + method) + .param("query", "ITEM")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item1), + ItemMatcher.matchItemProperties(item3) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test word for unauthorized item + getClient(tokenEPerson).perform(get("/api/core/items/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test eperson with no authorized items + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + getClient(tokenEPerson2).perform(get("/api/core/items/search/" + method) + .param("query", "community")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test query as admin + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/items/search/" + method) + .param("query", "sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item1), + ItemMatcher.matchItemProperties(item3) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test unsorted query words + getClient(tokenAdmin).perform(get("/api/core/items/search/" + method) + .param("query", "bitstreams sample")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains( + ItemMatcher.matchItemProperties(item3) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test item not authorized for eperson is returned for admin + getClient(tokenAdmin).perform(get("/api/core/items/search/" + method) + .param("query", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item2) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + } + } From b851840abfe999410ce372f2de0e56365be32b95 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Thu, 4 Dec 2025 15:02:44 +0100 Subject: [PATCH 539/701] Adapt getSubmitAuthorizedTypes to use inherited permissions --- .../dspace/content/EntityTypeServiceImpl.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java index 7df892cd56f5..1442c676a0c9 100644 --- a/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/EntityTypeServiceImpl.java @@ -16,6 +16,7 @@ import java.util.Set; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrServerException; import org.apache.solr.client.solrj.response.FacetField; @@ -28,6 +29,7 @@ import org.dspace.content.service.EntityTypeService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.discovery.SearchService; import org.dspace.discovery.SolrSearchCore; import org.dspace.discovery.indexobject.IndexableCollection; import org.dspace.eperson.EPerson; @@ -49,6 +51,9 @@ public class EntityTypeServiceImpl implements EntityTypeService { @Autowired protected SolrSearchCore solrSearchCore; + @Autowired + protected SearchService searchService; + @Override public EntityType findByEntityType(Context context, String entityType) throws SQLException { return entityTypeDAO.findByEntityType(context, entityType); @@ -126,26 +131,34 @@ public List getSubmitAuthorizedTypes(Context context) throws SQLException, SolrServerException, IOException { List types = new ArrayList<>(); StringBuilder query = null; - EPerson currentUser = context.getCurrentUser(); if (!authorizeService.isAdmin(context)) { - String userId = ""; + EPerson currentUser = context.getCurrentUser(); + StringBuilder epersonAndGroupClause = new StringBuilder(); if (currentUser != null) { - userId = currentUser.getID().toString(); - query = new StringBuilder(); - query.append("submit:(e").append(userId); + epersonAndGroupClause.append("e").append(currentUser.getID()); } - + //Retrieve all the groups the current user is a member of Set groups = groupService.allMemberGroupsSet(context, currentUser); for (Group group : groups) { - if (query == null) { - query = new StringBuilder(); - query.append("submit:(g"); + if (!epersonAndGroupClause.isEmpty()) { + epersonAndGroupClause.append(" OR g").append(group.getID()); } else { - query.append(" OR g"); + epersonAndGroupClause.append("g").append(group.getID()); } - query.append(group.getID()); } - query.append(")"); + + if (epersonAndGroupClause.isEmpty()) { + // No user or groups, no authorized types + return new ArrayList<>(); + } + query = new StringBuilder(); + query.append("submit:(").append(epersonAndGroupClause).append(")"); + query.append(" OR ").append("admin:(").append(epersonAndGroupClause).append(")"); + String locations = searchService.createLocationQueryForAdministrableDSOs(epersonAndGroupClause.toString()); + if (StringUtils.isNotBlank(locations)) { + query.append(" OR "); + query.append(locations); + } } SolrQuery sQuery = new SolrQuery("*:*"); From b73d34dd3e482fc124b8f44195c9d768d1e273d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:52:51 +0000 Subject: [PATCH 540/701] build(deps): bump jakarta.xml.bind:jakarta.xml.bind-api Bumps the jakarta group with 1 update: [jakarta.xml.bind:jakarta.xml.bind-api](https://github.com/jakartaee/jaxb-api). Updates `jakarta.xml.bind:jakarta.xml.bind-api` from 4.0.4 to 4.0.5 - [Release notes](https://github.com/jakartaee/jaxb-api/releases) - [Commits](https://github.com/jakartaee/jaxb-api/compare/4.0.4...4.0.5) --- updated-dependencies: - dependency-name: jakarta.xml.bind:jakarta.xml.bind-api dependency-version: 4.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aee1906f805e..6e4385fe69d3 100644 --- a/pom.xml +++ b/pom.xml @@ -35,7 +35,7 @@ 2.21.0 2.21 2.1.1 - 4.0.4 + 4.0.5 4.0.6 1.1.1 From 7e4d96e7dae307575b02c4ddc54b62901796924f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 09:54:59 +0000 Subject: [PATCH 541/701] build(deps): bump org.checkerframework:checker-qual Bumps [org.checkerframework:checker-qual](https://github.com/typetools/checker-framework) from 3.53.0 to 3.53.1. - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-3.53.0...checker-framework-3.53.1) --- updated-dependencies: - dependency-name: org.checkerframework:checker-qual dependency-version: 3.53.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index aee1906f805e..dc705902de6a 100644 --- a/pom.xml +++ b/pom.xml @@ -1366,7 +1366,7 @@ org.checkerframework checker-qual - 3.53.0 + 3.53.1 + + + + + @@ -645,4 +649,19 @@ + + + + + + https://orcid.org/ + ORCID + + + + + From b9321413e45e1a4912167c7128c912da781e1cd1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 18 Feb 2026 16:56:21 -0600 Subject: [PATCH 544/701] Install Apache Ant via apt-get instead of manual download. Manual download has a tendency to fail randomly in automated Docker builds. (cherry picked from commit d2abf0f7a0ab28af8d2f8c46782f637db2cf641a) --- Dockerfile | 15 +++++---------- Dockerfile.cli | 15 +++++---------- Dockerfile.test | 15 +++++---------- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/Dockerfile b/Dockerfile index 63b6af22682a..7f4472af284c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,16 +40,11 @@ ARG TARGET_DIR=dspace-installer # COPY the /install directory from 'build' container to /dspace-src in this container COPY --from=build /install /dspace-src WORKDIR /dspace-src -# Create the initial install deployment using ANT -ENV ANT_VERSION=1.10.13 -ENV ANT_HOME=/tmp/ant-$ANT_VERSION -ENV PATH=$ANT_HOME/bin:$PATH -# Download and install 'ant' -RUN mkdir $ANT_HOME && \ - curl --silent --show-error --location --fail --retry 5 --output /tmp/apache-ant.tar.gz \ - https://archive.apache.org/dist/ant/binaries/apache-ant-${ANT_VERSION}-bin.tar.gz && \ - tar -zx --strip-components=1 -f /tmp/apache-ant.tar.gz -C $ANT_HOME && \ - rm /tmp/apache-ant.tar.gz +# Install Apache Ant +RUN apt-get update \ + && apt-get install -y --no-install-recommends ant \ + && apt-get purge -y --auto-remove \ + && rm -rf /var/lib/apt/lists/* # Run necessary 'ant' deploy scripts RUN ant init_installation update_configs update_code update_webapps diff --git a/Dockerfile.cli b/Dockerfile.cli index d67c7644fd59..2c6c0f201aa0 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -34,16 +34,11 @@ ARG TARGET_DIR=dspace-installer # COPY the /install directory from 'build' container to /dspace-src in this container COPY --from=build /install /dspace-src WORKDIR /dspace-src -# Create the initial install deployment using ANT -ENV ANT_VERSION=1.10.13 -ENV ANT_HOME=/tmp/ant-$ANT_VERSION -ENV PATH=$ANT_HOME/bin:$PATH -# Download and install 'ant' -RUN mkdir $ANT_HOME && \ - curl --silent --show-error --location --fail --retry 5 --output /tmp/apache-ant.tar.gz \ - https://archive.apache.org/dist/ant/binaries/apache-ant-${ANT_VERSION}-bin.tar.gz && \ - tar -zx --strip-components=1 -f /tmp/apache-ant.tar.gz -C $ANT_HOME && \ - rm /tmp/apache-ant.tar.gz +# Install Apache Ant +RUN apt-get update \ + && apt-get install -y --no-install-recommends ant \ + && apt-get purge -y --auto-remove \ + && rm -rf /var/lib/apt/lists/* # Run necessary 'ant' deploy scripts RUN ant init_installation update_configs update_code diff --git a/Dockerfile.test b/Dockerfile.test index 220b15f85c19..79e2e5d9eec1 100644 --- a/Dockerfile.test +++ b/Dockerfile.test @@ -39,16 +39,11 @@ ARG TARGET_DIR=dspace-installer # COPY the /install directory from 'build' container to /dspace-src in this container COPY --from=build /install /dspace-src WORKDIR /dspace-src -# Create the initial install deployment using ANT -ENV ANT_VERSION=1.10.12 -ENV ANT_HOME=/tmp/ant-$ANT_VERSION -ENV PATH=$ANT_HOME/bin:$PATH -# Download and install 'ant' -RUN mkdir $ANT_HOME && \ - curl --silent --show-error --location --fail --retry 5 --output /tmp/apache-ant.tar.gz \ - https://archive.apache.org/dist/ant/binaries/apache-ant-${ANT_VERSION}-bin.tar.gz && \ - tar -zx --strip-components=1 -f /tmp/apache-ant.tar.gz -C $ANT_HOME && \ - rm /tmp/apache-ant.tar.gz +# Install Apache Ant +RUN apt-get update \ + && apt-get install -y --no-install-recommends ant \ + && apt-get purge -y --auto-remove \ + && rm -rf /var/lib/apt/lists/* # Run necessary 'ant' deploy scripts RUN ant init_installation update_configs update_code update_webapps From 14599222e17f876ef047d9678cbbf5699e1c9e3d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:07:18 +0000 Subject: [PATCH 545/701] build(deps): bump the spring group with 13 updates Bumps the spring group with 13 updates: | Package | From | To | | --- | --- | --- | | [org.springframework:spring-orm](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-core](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-beans](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-aop](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-context](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-context-support](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-tx](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-jdbc](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-web](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-expression](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework:spring-test](https://github.com/spring-projects/spring-framework) | `6.2.15` | `6.2.16` | | [org.springframework.ldap:spring-ldap-core](https://github.com/spring-projects/spring-ldap) | `3.3.5` | `3.3.6` | Updates `org.springframework:spring-orm` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-core` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-beans` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-aop` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-context` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-context-support` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-tx` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-jdbc` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-web` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-webmvc` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-expression` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-test` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-core` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-beans` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-aop` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-context` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-context-support` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework.ldap:spring-ldap-core` from 3.3.5 to 3.3.6 - [Release notes](https://github.com/spring-projects/spring-ldap/releases) - [Changelog](https://github.com/spring-projects/spring-ldap/blob/main/changelog.txt) - [Commits](https://github.com/spring-projects/spring-ldap/compare/3.3.5...3.3.6) Updates `org.springframework:spring-tx` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-jdbc` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-web` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-webmvc` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-expression` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) Updates `org.springframework:spring-test` from 6.2.15 to 6.2.16 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.15...v6.2.16) --- updated-dependencies: - dependency-name: org.springframework:spring-orm dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.ldap:spring-ldap-core dependency-version: 3.3.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.16 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring ... Signed-off-by: dependabot[bot] --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e49a274d5a0e..85fb0af2bce7 100644 --- a/pom.xml +++ b/pom.xml @@ -19,8 +19,8 @@ 17 - 6.2.15 - 3.3.5 + 6.2.16 + 3.3.6 3.5.10 6.5.7 6.4.10.Final From 7a78766e8f4813c37bcd72c90dd149b014d0f9f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:09:51 +0000 Subject: [PATCH 546/701] build(deps): bump org.postgresql:postgresql from 42.7.9 to 42.7.10 Bumps [org.postgresql:postgresql](https://github.com/pgjdbc/pgjdbc) from 42.7.9 to 42.7.10. - [Release notes](https://github.com/pgjdbc/pgjdbc/releases) - [Changelog](https://github.com/pgjdbc/pgjdbc/blob/master/CHANGELOG.md) - [Commits](https://github.com/pgjdbc/pgjdbc/compare/REL42.7.9...REL42.7.10) --- updated-dependencies: - dependency-name: org.postgresql:postgresql dependency-version: 42.7.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e49a274d5a0e..300c5630fd02 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ 6.5.7 6.4.10.Final 8.0.3.Final - 42.7.9 + 42.7.10 10.22.0 8.11.4 From 38fdd77ba372cf466ec4f6b066ee94a8b3153719 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:13:55 +0000 Subject: [PATCH 547/701] build(deps): bump the amazon-s3 group with 2 updates Bumps the amazon-s3 group with 2 updates: software.amazon.awssdk:s3 and [software.amazon.awssdk.crt:aws-crt](https://github.com/awslabs/aws-crt-java). Updates `software.amazon.awssdk:s3` from 2.41.14 to 2.41.24 Updates `software.amazon.awssdk.crt:aws-crt` from 0.42.2 to 0.43.1 - [Release notes](https://github.com/awslabs/aws-crt-java/releases) - [Commits](https://github.com/awslabs/aws-crt-java/compare/v0.42.2...v0.43.1) --- updated-dependencies: - dependency-name: software.amazon.awssdk:s3 dependency-version: 2.41.24 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: amazon-s3 - dependency-name: software.amazon.awssdk.crt:aws-crt dependency-version: 0.43.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: amazon-s3 ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 22b752dd9e1e..3b4dde1b37a2 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -722,7 +722,7 @@ software.amazon.awssdk s3 - 2.41.14 + 2.41.28 software.amazon.awssdk @@ -738,7 +738,7 @@ software.amazon.awssdk.crt aws-crt - 0.42.2 + 0.43.1 + org.jboss.logging jboss-logging - 3.6.1.Final + 3.5.0.Final From 888825b1db9e81a9533fbb24029e201e00a71c64 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 23 Feb 2026 09:17:15 -0600 Subject: [PATCH 549/701] Fix Handle Server startup issues by excluding spring-jcl from spring-core. Older spring-jcl exclusions can now be removed as they all inherit via spring-core (cherry picked from commit 5a7d4a93ef78f5b7ac364d363fac3285f8270318) --- dspace-api/pom.xml | 7 ------- dspace-iiif/pom.xml | 5 ----- dspace-oai/pom.xml | 5 ----- dspace-rdf/pom.xml | 5 ----- dspace-server-webapp/pom.xml | 5 ----- dspace-services/pom.xml | 7 ------- dspace-sword/pom.xml | 5 ----- dspace-swordv2/pom.xml | 5 ----- pom.xml | 15 ++++++++------- 9 files changed, 8 insertions(+), 51 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 3b4dde1b37a2..0499632cf246 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -386,13 +386,6 @@ org.springframework spring-orm - - - - org.springframework - spring-jcl - - diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index f84acfe5a247..56a43c395df4 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -44,11 +44,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index baf8f1616acd..7713769d2461 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -78,11 +78,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 8af62625ae27..5d4b6c119c31 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -67,11 +67,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 273ea8d78dcd..f69507d2cee3 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -460,11 +460,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 5c3ff226c22c..9557a6206a05 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -90,13 +90,6 @@ spring-context-support ${spring.version} compile - - - - org.springframework - spring-jcl - - org.apache.commons diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index d1d625d4f576..bfb54a7db5a1 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -46,11 +46,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index a6ce18ed6a93..f5921baa4a88 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -83,11 +83,6 @@ org.springframework.boot spring-boot-starter-logging - - - org.springframework - spring-jcl - diff --git a/pom.xml b/pom.xml index 87adf570cb65..62ed679801d2 100644 --- a/pom.xml +++ b/pom.xml @@ -1171,13 +1171,6 @@ org.springframework spring-orm ${spring.version} - - - - org.springframework - spring-jcl - - @@ -1185,6 +1178,14 @@ spring-core org.springframework ${spring.version} + + + + org.springframework + spring-jcl + + From 94cf8e2e429fbc4e5380a46378443f40dbf1bef6 Mon Sep 17 00:00:00 2001 From: Abhinav S <42234845+AbhinavS96@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:42:18 +0000 Subject: [PATCH 550/701] make dc.type match stricter (#11994) Co-authored-by: abhinav --- dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index e9699d8e0160..75031b3927e9 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -803,7 +803,7 @@ - + @@ -1461,7 +1461,7 @@ literature - + dataset From c6702ad74f2dae559d6caa25e2961eac717a6a51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:13:04 +0000 Subject: [PATCH 551/701] build(deps): bump github/codeql-action from 3 to 4 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codescan.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index cbdb4b880cbb..7a29297b1ebd 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. # https://github.com/github/codeql-action - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@v4 with: # Codescan Javascript as well since a few JS files exist in REST API's interface languages: java, javascript @@ -56,8 +56,8 @@ jobs: # NOTE: Based on testing, this autobuild process works well for DSpace. A custom # DSpace build w/caching (like in build.yml) was about the same speed as autobuild. - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@v4 # Perform GitHub Code Scanning. - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@v4 From be0cd30b976a41ce13b5a59f137f476a3fc6ac1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:33:20 +0100 Subject: [PATCH 552/701] build(deps): bump actions/checkout from 4 to 6 (#12033) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/codescan.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/port_merged_pull_request.yml | 2 +- .github/workflows/reusable-docker-build.yml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39a6f41429fd..395bd086a519 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: steps: # https://github.com/actions/checkout - name: Checkout codebase - uses: actions/checkout@v4 + uses: actions/checkout@v6 # https://github.com/actions/setup-java - name: Install JDK ${{ matrix.java }} @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Download artifacts from previous 'tests' job - name: Download coverage artifacts diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index cbdb4b880cbb..ffcbf3c24e7e 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -35,7 +35,7 @@ jobs: steps: # https://github.com/actions/checkout - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 # https://github.com/actions/setup-java - name: Install JDK diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index aea0bb6478ff..f1d0edfb2bed 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -183,7 +183,7 @@ jobs: steps: # Checkout our codebase (to get access to Docker Compose scripts) - name: Checkout codebase - uses: actions/checkout@v4 + uses: actions/checkout@v6 # Download Docker image artifacts (which were just built by reusable-docker-build.yml) - name: Download Docker image artifacts uses: actions/download-artifact@v4 diff --git a/.github/workflows/port_merged_pull_request.yml b/.github/workflows/port_merged_pull_request.yml index 857f22755e49..6c63ffa83129 100644 --- a/.github/workflows/port_merged_pull_request.yml +++ b/.github/workflows/port_merged_pull_request.yml @@ -23,7 +23,7 @@ jobs: if: github.event.pull_request.merged steps: # Checkout code - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # Port PR to other branch (ONLY if labeled with "port to") # See https://github.com/korthout/backport-action - name: Create backport pull requests diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 5e011c4668ed..ecae833feaa4 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -109,7 +109,7 @@ jobs: # https://github.com/actions/checkout - name: Checkout codebase - uses: actions/checkout@v4 + uses: actions/checkout@v6 # https://github.com/docker/login-action # NOTE: This login occurs for BOTH non-PRs or PRs. PRs *must* also login to access private images from GHCR From b65ca568eaf9555ffa23f0a7ab9d30231b2b5db1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:33:52 +0100 Subject: [PATCH 553/701] build(deps): bump actions/add-to-project from 1.0.0 to 1.0.2 (#12034) Bumps [actions/add-to-project](https://github.com/actions/add-to-project) from 1.0.0 to 1.0.2. - [Release notes](https://github.com/actions/add-to-project/releases) - [Commits](https://github.com/actions/add-to-project/compare/v1.0.0...v1.0.2) --- updated-dependencies: - dependency-name: actions/add-to-project dependency-version: 1.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/issue_opened.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index 0a35a6a95044..c8c421d98f47 100644 --- a/.github/workflows/issue_opened.yml +++ b/.github/workflows/issue_opened.yml @@ -16,7 +16,7 @@ jobs: # Only add to project board if issue is flagged as "needs triage" or has no labels # NOTE: By default we flag new issues as "needs triage" in our issue template if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') - uses: actions/add-to-project@v1.0.0 + uses: actions/add-to-project@v1.0.2 # Note, the authentication token below is an ORG level Secret. # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token From 0ffcd7d4df37173980a2f25265d6f26f006ec1ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:34:06 +0100 Subject: [PATCH 554/701] build(deps): bump the amazon-s3 group with 2 updates (#12027) Bumps the amazon-s3 group with 2 updates: software.amazon.awssdk:s3 and [software.amazon.awssdk.crt:aws-crt](https://github.com/awslabs/aws-crt-java). Updates `software.amazon.awssdk:s3` from 2.41.28 to 2.41.33 Updates `software.amazon.awssdk.crt:aws-crt` from 0.43.1 to 0.43.4 - [Release notes](https://github.com/awslabs/aws-crt-java/releases) - [Commits](https://github.com/awslabs/aws-crt-java/compare/v0.43.1...v0.43.4) --- updated-dependencies: - dependency-name: software.amazon.awssdk:s3 dependency-version: 2.41.33 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: amazon-s3 - dependency-name: software.amazon.awssdk.crt:aws-crt dependency-version: 0.43.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: amazon-s3 ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dspace-api/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 0499632cf246..972f392a866d 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -715,7 +715,7 @@ software.amazon.awssdk s3 - 2.41.28 + 2.41.33 software.amazon.awssdk @@ -731,7 +731,7 @@ software.amazon.awssdk.crt aws-crt - 0.43.1 + 0.43.4 + 3.5.11 + 6.5.8 6.4.10.Final 8.0.3.Final 42.7.10 From fd88b6284c7fa41cc406c8bb9b468b72fc28b14f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:47:07 +0000 Subject: [PATCH 556/701] build(deps): bump actions/setup-java from 4 to 5 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-java dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- .github/workflows/codescan.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 395bd086a519..8d7a58b09028 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: # https://github.com/actions/setup-java - name: Install JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} distribution: 'temurin' diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index ffcbf3c24e7e..5b79f6464e10 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -39,7 +39,7 @@ jobs: # https://github.com/actions/setup-java - name: Install JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 17 distribution: 'temurin' From 40e37c86e5e667bcd191f90c0c6c29099a134d81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:52:37 +0000 Subject: [PATCH 557/701] build(deps): bump korthout/backport-action from 2 to 4 Bumps [korthout/backport-action](https://github.com/korthout/backport-action) from 2 to 4. - [Release notes](https://github.com/korthout/backport-action/releases) - [Commits](https://github.com/korthout/backport-action/compare/v2...v4) --- updated-dependencies: - dependency-name: korthout/backport-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/port_merged_pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/port_merged_pull_request.yml b/.github/workflows/port_merged_pull_request.yml index 6c63ffa83129..676ad45ba263 100644 --- a/.github/workflows/port_merged_pull_request.yml +++ b/.github/workflows/port_merged_pull_request.yml @@ -27,7 +27,7 @@ jobs: # Port PR to other branch (ONLY if labeled with "port to") # See https://github.com/korthout/backport-action - name: Create backport pull requests - uses: korthout/backport-action@v2 + uses: korthout/backport-action@v4 with: # Trigger based on a "port to [branch]" label on PR # (This label must specify the branch name to port to) From 873eb54b41e1dc4c5ec1d333445d419425057ce9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:45:16 +0000 Subject: [PATCH 558/701] build(deps): bump the build-tools group with 4 updates Bumps the build-tools group with 4 updates: [org.apache.maven.plugins:maven-compiler-plugin](https://github.com/apache/maven-compiler-plugin), [org.apache.maven.plugins:maven-surefire-plugin](https://github.com/apache/maven-surefire), [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) and [org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin). Updates `org.apache.maven.plugins:maven-compiler-plugin` from 3.14.1 to 3.15.0 - [Release notes](https://github.com/apache/maven-compiler-plugin/releases) - [Commits](https://github.com/apache/maven-compiler-plugin/compare/maven-compiler-plugin-3.14.1...maven-compiler-plugin-3.15.0) Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5) Updates `org.apache.maven.plugins:maven-failsafe-plugin` from 3.5.4 to 3.5.5 - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5) Updates `org.apache.maven.plugins:maven-dependency-plugin` from 3.9.0 to 3.10.0 - [Release notes](https://github.com/apache/maven-dependency-plugin/releases) - [Commits](https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.9.0...maven-dependency-plugin-3.10.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-compiler-plugin dependency-version: 3.15.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-failsafe-plugin dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: build-tools - dependency-name: org.apache.maven.plugins:maven-dependency-plugin dependency-version: 3.10.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index ded806e6b2dd..15a2c050102f 100644 --- a/pom.xml +++ b/pom.xml @@ -143,7 +143,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.14.1 + 3.15.0 ${java.version} @@ -211,7 +211,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.4 + 3.5.5 @@ -238,7 +238,7 @@ maven-failsafe-plugin - 3.5.4 + 3.5.5 @@ -351,7 +351,7 @@ org.apache.maven.plugins maven-dependency-plugin - 3.9.0 + 3.10.0 org.apache.maven.plugins From 955e664373e426be6f23c141e4fee503064f6966 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:46:27 +0000 Subject: [PATCH 559/701] build(deps): bump the fasterxml group with 3 updates Bumps the fasterxml group with 3 updates: [com.fasterxml.jackson.core:jackson-core](https://github.com/FasterXML/jackson-core), [com.fasterxml.jackson.core:jackson-databind](https://github.com/FasterXML/jackson) and com.fasterxml.jackson.datatype:jackson-datatype-jsr310. Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.0 to 2.21.1 - [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.0...jackson-core-2.21.1) Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.0 to 2.21.1 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.21.0 to 2.21.1 Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.0 to 2.21.1 - [Commits](https://github.com/FasterXML/jackson/commits) Updates `com.fasterxml.jackson.datatype:jackson-datatype-jsr310` from 2.21.0 to 2.21.1 --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-core dependency-version: 2.21.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.21.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency-version: 2.21.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-version: 2.21.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml - dependency-name: com.fasterxml.jackson.datatype:jackson-datatype-jsr310 dependency-version: 2.21.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: fasterxml ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ded806e6b2dd..7b5e4026bd86 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 3.11.1 2.42.0 - 2.21.0 + 2.21.1 2.21 2.1.1 4.0.5 From 9e8aa61780c7537442afadf72780227694e46d29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:49:32 +0000 Subject: [PATCH 560/701] build(deps): bump docker/build-push-action from 5 to 6 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/reusable-docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index ecae833feaa4..efcd7f6ddd80 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -147,7 +147,7 @@ jobs: - name: Build and push image to ${{ env.DOCKER_BUILD_REGISTRY }} if: ${{ ! matrix.isPr }} id: docker_build - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: build-contexts: | ${{ inputs.dockerfile_additional_contexts }} @@ -201,7 +201,7 @@ jobs: # NOTE: This step cannot be combined with the build above as it's a different type of output. - name: Build and push image to local TAR file if: ${{ matrix.arch == 'linux/amd64'}} - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: build-contexts: | ${{ inputs.dockerfile_additional_contexts }} From 55a4b44059752fc2303bff8eda6960ba8bceebae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:49:38 +0000 Subject: [PATCH 561/701] build(deps): bump Wandalen/wretry.action from 1.3.0 to 3.8.0 Bumps [Wandalen/wretry.action](https://github.com/wandalen/wretry.action) from 1.3.0 to 3.8.0. - [Release notes](https://github.com/wandalen/wretry.action/releases) - [Commits](https://github.com/wandalen/wretry.action/compare/v1.3.0...v3.8.0) --- updated-dependencies: - dependency-name: Wandalen/wretry.action dependency-version: 3.8.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 395bd086a519..b2cd86ea549b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,7 +99,7 @@ jobs: # Retry action: https://github.com/marketplace/actions/retry-action # Codecov action: https://github.com/codecov/codecov-action - name: Upload coverage to Codecov.io - uses: Wandalen/wretry.action@v1.3.0 + uses: Wandalen/wretry.action@v3.8.0 with: action: codecov/codecov-action@v4 # Ensure codecov-action throws an error when it fails to upload From a52e2e2b49b04244fbf5744f560199573fa8391f Mon Sep 17 00:00:00 2001 From: MMilosz Date: Mon, 2 Mar 2026 16:23:21 +0100 Subject: [PATCH 562/701] chore(submission): set repeatable=false for dc.description in bitstream-metadata #12039 (cherry picked from commit c301804561a6cedb4256a661fed262566856c638) --- dspace/config/submission-forms.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index 0c5d00e4aa62..799b47a377a3 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -39,7 +39,7 @@ dc description - true + false textarea Enter a description for the file From 341d557a8e753426049ce174d961086ff7d266ed Mon Sep 17 00:00:00 2001 From: wout Date: Fri, 14 Mar 2025 16:27:53 +0100 Subject: [PATCH 563/701] 127822: update researcherid -> webofscience (cherry picked from commit 95dc45362af68cb88852365b3b9eccd81b285d48) --- dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 75031b3927e9..39b61318d464 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -369,7 +369,7 @@ + schemeURI="https://www.webofscience.com"> From f2bf2fdfe558400df2d14eb1f4ef78fee9297087 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:44:31 +0000 Subject: [PATCH 564/701] build(deps): bump actions/download-artifact from 4 to 8 Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- .github/workflows/docker.yml | 2 +- .github/workflows/reusable-docker-build.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 870cad3ee7c9..b66d0d01f48d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -91,7 +91,7 @@ jobs: # Download artifacts from previous 'tests' job - name: Download coverage artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 # Now attempt upload to Codecov using its action. # NOTE: We use a retry action to retry the Codecov upload if it fails the first time. diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index f1d0edfb2bed..b3f67ad8e479 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -186,7 +186,7 @@ jobs: uses: actions/checkout@v6 # Download Docker image artifacts (which were just built by reusable-docker-build.yml) - name: Download Docker image artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: # Download all amd64 Docker images (TAR files) into the /tmp/docker directory pattern: docker-image-*-linux-amd64 diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index efcd7f6ddd80..98ee15d57f65 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -245,7 +245,7 @@ jobs: - docker-build steps: - name: Download Docker build digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: /tmp/digests # Download digests for both AMD64 and ARM64 into same directory From 2df60af78cc2083f90ab864f5bf95c3b33a1e804 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:44:38 +0000 Subject: [PATCH 565/701] build(deps): bump toshimaru/auto-author-assign from 2.1.0 to 3.0.1 Bumps [toshimaru/auto-author-assign](https://github.com/toshimaru/auto-author-assign) from 2.1.0 to 3.0.1. - [Release notes](https://github.com/toshimaru/auto-author-assign/releases) - [Changelog](https://github.com/toshimaru/auto-author-assign/blob/main/CHANGELOG.md) - [Commits](https://github.com/toshimaru/auto-author-assign/compare/v2.1.0...v3.0.1) --- updated-dependencies: - dependency-name: toshimaru/auto-author-assign dependency-version: 3.0.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pull_request_opened.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml index bbac52af2438..e2b6e8ba9c2c 100644 --- a/.github/workflows/pull_request_opened.yml +++ b/.github/workflows/pull_request_opened.yml @@ -21,4 +21,4 @@ jobs: # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards # See https://github.com/toshimaru/auto-author-assign - name: Assign PR to creator - uses: toshimaru/auto-author-assign@v2.1.0 + uses: toshimaru/auto-author-assign@v3.0.1 From 64a3ae85848579a1a2b3d8ce90f09233d9f5b5c2 Mon Sep 17 00:00:00 2001 From: abhinav Date: Tue, 3 Mar 2026 10:45:29 +0100 Subject: [PATCH 566/701] fix multiple empty datasite:sizes tags (cherry picked from commit 1a93f9a60b07acf5e58a24b0f3a9a320616d91db) --- .../crosswalks/oai/metadataFormats/oai_openaire.xsl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 39b61318d464..572af5278a3e 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -858,13 +858,13 @@ - - + + - - + + From 81bc0a6e06faa99e333749e59f70819aab7a2c6b Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Fri, 20 Feb 2026 10:31:25 +0100 Subject: [PATCH 567/701] Update authorized Solr searches to escape queries and include dc.title_sort for queries with spaces (cherry picked from commit c1bcd0e7845b223b3ee136359a275880bf8d8004) --- .../authorize/AuthorizeServiceImpl.java | 4 ++ .../dspace/content/CollectionServiceImpl.java | 8 +--- .../org/dspace/content/ItemServiceImpl.java | 3 ++ .../org/dspace/discovery/SearchService.java | 9 ++++ .../org/dspace/discovery/SolrServiceImpl.java | 21 +++++++++ .../app/rest/CollectionRestRepositoryIT.java | 42 ++++++++++++++++-- .../app/rest/CommunityRestRepositoryIT.java | 41 +++++++++++++++-- .../dspace/app/rest/ItemRestRepositoryIT.java | 44 +++++++++++++++++-- 8 files changed, 156 insertions(+), 16 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java index 74ad6f6e82b1..bb94198f7961 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -825,6 +825,7 @@ public List findAuthorizedCommunityByAction(Context context, String q int limit) throws SearchServiceException { List communities = new ArrayList<>(); + query = searchService.formatAutoCompleteQuery(query, "dc.title_sort"); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, action, true, @@ -862,6 +863,7 @@ public long countAdminAuthorizedCommunity(Context context, String query) @Override public long countAuthorizedCommunityByAction(Context context, String query, int action) throws SearchServiceException { + query = searchService.formatAutoCompleteQuery(query, "dc.title_sort"); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCommunity.TYPE, action, true, @@ -905,6 +907,7 @@ public List findAuthorizedCollectionByAction(Context context, String return collections; } + query = searchService.formatAutoCompleteQuery(query, "dc.title_sort"); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, action, true, @@ -942,6 +945,7 @@ public long countAdminAuthorizedCollection(Context context, String query) @Override public long countAuthorizedCollectionByAction(Context context, String query, int action) throws SearchServiceException { + query = searchService.formatAutoCompleteQuery(query, "dc.title_sort"); query = formatCustomQuery(query); DiscoverResult discoverResult = getDiscoverResult(context, query + RESOURCE_TYPE_FIELD + ":" + IndexableCollection.TYPE, action, diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index 7531f748adf4..2f3fd827bfab 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -28,7 +28,6 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; -import org.apache.solr.client.solrj.util.ClientUtils; import org.dspace.app.util.AuthorizeUtil; import org.dspace.authorize.AuthorizeConfiguration; import org.dspace.authorize.AuthorizeException; @@ -1086,11 +1085,8 @@ private DiscoverResult retrieveCollectionsWithSubmit(Context context, DiscoverQu discoverQuery.addFilterQueries("search.entitytype:" + entityType); } if (StringUtils.isNotBlank(q)) { - StringBuilder buildQuery = new StringBuilder(); - String escapedQuery = ClientUtils.escapeQueryChars(q); - buildQuery.append("(").append(escapedQuery).append(" OR dc.title_sort:*") - .append(escapedQuery).append("*").append(")"); - discoverQuery.setQuery(buildQuery.toString()); + q = searchService.formatAutoCompleteQuery(q, "dc.title_sort"); + discoverQuery.setQuery(q); } discoverQuery.addRequiredAuthorization(Constants.ADD); DiscoverResult resp = searchService.search(context, discoverQuery); diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 23e08800a9d4..f8ea21b3cccb 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -1267,6 +1267,9 @@ public boolean canEdit(Context context, Item item) throws SQLException { private DiscoverResult retrieveItemsWithEdit(Context context, DiscoverQuery discoverQuery, String q) throws SearchServiceException { if (StringUtils.isNotBlank(q)) { + // Although not all items will have a metadata dc.title, we use it for autocomplete because it is the + // most common. Ideally, we should use a field that all indexed items have + q = searchService.formatAutoCompleteQuery(q, "dc.title_sort"); discoverQuery.setQuery(q); } discoverQuery.addRequiredAuthorization(Constants.WRITE); diff --git a/dspace-api/src/main/java/org/dspace/discovery/SearchService.java b/dspace-api/src/main/java/org/dspace/discovery/SearchService.java index 7f4c85c3a864..986b2d23de2a 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SearchService.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SearchService.java @@ -126,6 +126,15 @@ List getRelatedItems(Context context, Item item, */ String escapeQueryChars(String query); + /** + * Utility method to format an autocomplete query over a specific field. + * + * @param query to search for + * @param autocompleteField the field to use to autocomplete search, if null or empty no field is used + * @return the constructed solr query + */ + String formatAutoCompleteQuery(String query, String autocompleteField); + FacetYearRange getFacetYearRange(Context context, IndexableObject scope, DiscoverySearchFilterFacet facet, List filterQueries, DiscoverQuery parentQuery) throws SearchServiceException; diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java index c3b5cc78a243..88437ca7b8dc 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java @@ -1676,6 +1676,27 @@ public String escapeQueryChars(String query) { return ClientUtils.escapeQueryChars(query); } + /** + * Utility method to format an autocomplete query over a specific field. Combines the escaped query with a + * wildcard search over the specified {@code autocompleteField}. This field is typically non-tokenized and + * allows recovering searches containing spaces as a single value. + * + * @param query the user input to search for + * @param autocompleteField non-tokenized field used for wildcard autocomplete + * @return the constructed Solr query, or the original query if blank + */ + @Override + public String formatAutoCompleteQuery(String query, String autocompleteField) { + if (StringUtils.isNotBlank(query)) { + StringBuilder buildQuery = new StringBuilder(); + String escapedQuery = escapeQueryChars(query); + buildQuery.append("(").append(escapedQuery).append(" OR ").append(autocompleteField).append(":*") + .append(escapedQuery).append("*").append(")"); + return buildQuery.toString(); + } + return query; + } + @Override public FacetYearRange getFacetYearRange(Context context, IndexableObject scope, DiscoverySearchFilterFacet facet, List filterQueries, diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java index 1a66a15b6f2e..90ee769d42b9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java @@ -3948,6 +3948,13 @@ public void findGenericAuthorizedCollectionsWithQueryTest(String method) throws .withName("Collection of sample items") .withAdminGroup(eperson) .build(); + Collection col4 = CollectionBuilder.createCollection(context, child2) + .withName("Testing autocomplete in collection") + .withAdminGroup(eperson2) + .build(); + Collection col5 = CollectionBuilder.createCollection(context, child2) + .withName("Title: subtitle (special characters)") + .build(); context.restoreAuthSystemState(); String tokenEPerson = getAuthToken(eperson.getEmail(), password); @@ -3978,11 +3985,29 @@ public void findGenericAuthorizedCollectionsWithQueryTest(String method) throws .andExpect(jsonPath("$.page.totalElements", is(0))); // Test eperson with no authorized collections + getClient(tokenEPerson).perform(get("/api/core/collections/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + // Test eperson2 gets only their authorized collection getClient(tokenEPerson2).perform(get("/api/core/collections/search/" + method) - .param("query", "collection")) + .param("query", "auto")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", is(0))); + .andExpect(jsonPath("$._embedded.collections", Matchers.contains( + CollectionMatcher.matchProperties(col4.getName(), col4.getID(), col4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test query with multiple words + getClient(tokenEPerson2).perform(get("/api/core/collections/search/" + method) + .param("query", "testing auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( + CollectionMatcher.matchProperties(col4.getName(), col4.getID(), col4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); // Test admin gets all authorized collections String tokenAdmin = getAuthToken(admin.getEmail(), password); @@ -4009,10 +4034,19 @@ public void findGenericAuthorizedCollectionsWithQueryTest(String method) throws .param("query", "test")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.collections", Matchers.containsInAnyOrder( - CollectionMatcher.matchProperties(col2.getName(), col2.getID(), col2.getHandle()) + CollectionMatcher.matchProperties(col2.getName(), col2.getID(), col2.getHandle()), + CollectionMatcher.matchProperties(col4.getName(), col4.getID(), col4.getHandle()) ))) - .andExpect(jsonPath("$.page.totalElements", is(1))); + .andExpect(jsonPath("$.page.totalElements", is(2))); + // Test query with special characters + getClient(tokenAdmin).perform(get("/api/core/collections/search/" + method) + .param("query", "title: subtitle (special")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.collections", Matchers.contains( + CollectionMatcher.matchProperties(col5.getName(), col5.getID(), col5.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java index 6e446e6de0a2..74c5c4cf4d76 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java @@ -3177,6 +3177,13 @@ public void findGenericAuthorizedCommunitiesWithQueryTest(String method) throws .withName("community of sample items") .withAdminGroup(eperson) .build(); + Community com4 = CommunityBuilder.createSubCommunity(context, child2) + .withName("Testing autocomplete in community") + .withAdminGroup(eperson2) + .build(); + Community com5 = CommunityBuilder.createSubCommunity(context, child2) + .withName("Title: subtitle (special characters)") + .build(); context.restoreAuthSystemState(); // Test simple query @@ -3207,11 +3214,29 @@ public void findGenericAuthorizedCommunitiesWithQueryTest(String method) throws .andExpect(jsonPath("$.page.totalElements", is(0))); // Test eperson with no authorized communities + getClient(tokenEPerson).perform(get("/api/core/communities/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + // Test eperson2 gets only their authorized community getClient(tokenEPerson2).perform(get("/api/core/communities/search/" + method) - .param("query", "community")) + .param("query", "auto")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", is(0))); + .andExpect(jsonPath("$._embedded.communities", Matchers.contains( + CommunityMatcher.matchProperties(com4.getName(), com4.getID(), com4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test query with multiple words + getClient(tokenEPerson2).perform(get("/api/core/communities/search/" + method) + .param("query", "testing auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( + CommunityMatcher.matchProperties(com4.getName(), com4.getID(), com4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); // Test as admin String tokenAdmin = getAuthToken(admin.getEmail(), password); @@ -3238,7 +3263,17 @@ public void findGenericAuthorizedCommunitiesWithQueryTest(String method) throws .param("query", "test")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.communities", Matchers.containsInAnyOrder( - CommunityMatcher.matchProperties(com2.getName(), com2.getID(), com2.getHandle()) + CommunityMatcher.matchProperties(com2.getName(), com2.getID(), com2.getHandle()), + CommunityMatcher.matchProperties(com4.getName(), com4.getID(), com4.getHandle()) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test query with special characters + getClient(tokenAdmin).perform(get("/api/core/communities/search/" + method) + .param("query", "title: subtitle")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.communities", Matchers.contains( + CommunityMatcher.matchProperties(com5.getName(), com5.getID(), com5.getHandle()) ))) .andExpect(jsonPath("$.page.totalElements", is(1))); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index 38e7fd9a2b54..cb31454f051e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -5089,6 +5089,10 @@ public void findGenericAuthorizedItemsWithQueryTest(String method) throws Except .withName("col3") .withAdminGroup(eperson) .build(); + Collection col4 = CollectionBuilder.createCollection(context, child2) + .withName("col4") + .withAdminGroup(eperson2) + .build(); Item item1 = ItemBuilder.createItem(context, col1) .withTitle("Sample item") .build(); @@ -5098,6 +5102,12 @@ public void findGenericAuthorizedItemsWithQueryTest(String method) throws Except Item item3 = ItemBuilder.createItem(context, col3) .withTitle("Item of sample bitstreams") .build(); + Item item4 = ItemBuilder.createItem(context, col4) + .withTitle("Testing autocomplete in items") + .build(); + Item item5 = ItemBuilder.createItem(context, col4) + .withTitle("Title: subtitle (special characters)") + .build(); context.restoreAuthSystemState(); @@ -5129,11 +5139,29 @@ public void findGenericAuthorizedItemsWithQueryTest(String method) throws Except .andExpect(jsonPath("$.page.totalElements", is(0))); // Test eperson with no authorized items + getClient(tokenEPerson).perform(get("/api/core/items/search/" + method) + .param("query", "auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + String tokenEPerson2 = getAuthToken(eperson2.getEmail(), password); + // Test eperson2 with one authorized item getClient(tokenEPerson2).perform(get("/api/core/items/search/" + method) - .param("query", "community")) + .param("query", "auto")) .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", is(0))); + .andExpect(jsonPath("$._embedded.items", Matchers.contains( + ItemMatcher.matchItemProperties(item4) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Test query with multiple words + getClient(tokenEPerson2).perform(get("/api/core/items/search/" + method) + .param("query", "testing auto")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( + ItemMatcher.matchItemProperties(item4) + ))) + .andExpect(jsonPath("$.page.totalElements", is(1))); // Test query as admin String tokenAdmin = getAuthToken(admin.getEmail(), password); @@ -5160,7 +5188,17 @@ public void findGenericAuthorizedItemsWithQueryTest(String method) throws Except .param("query", "test")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.items", Matchers.containsInAnyOrder( - ItemMatcher.matchItemProperties(item2) + ItemMatcher.matchItemProperties(item2), + ItemMatcher.matchItemProperties(item4) + ))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + // Test special characters in query + getClient(tokenAdmin).perform(get("/api/core/items/search/" + method) + .param("query", "title: subtitle (special")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.items", Matchers.contains( + ItemMatcher.matchItemProperties(item5) ))) .andExpect(jsonPath("$.page.totalElements", is(1))); From 25b35aead24cf320cf576d4fafa0a14ea1510a5e Mon Sep 17 00:00:00 2001 From: Piaget Bouaka Donfack Date: Tue, 23 Sep 2025 12:45:50 +0200 Subject: [PATCH 568/701] [DURACOM-389] Import from external sources creates invalid metadata (cherry picked from commit 255b1dd15ef926b88361ab6c1ad3ec3ed7f6f30f) --- .../external/crossref/CrossRefAbstractProcessor.java | 8 ++++++++ .../contributor/SimpleJsonPathMetadataContributor.java | 3 +++ 2 files changed, 11 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java index 99f1ee37a54e..d54c70c31984 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAbstractProcessor.java @@ -95,6 +95,14 @@ private String prettifyAbstract(String abstractValue) { sb.append("\n"); } sb.append("\n"); + } else if (StringUtils.equals(nodeName, "jats:p")) { + NodeList secElements = childElement.getChildNodes(); + for (int j = 0; j < secElements.getLength(); j++) { + Node secChildElement = secElements.item(j); + sb.append(secChildElement.getTextContent()); + sb.append("\n"); + } + sb.append("\n"); } } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java index 590fc63283b9..db3ba16dc42c 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java @@ -146,6 +146,9 @@ public Collection contributeMetadata(String fullJson) { } } for (String value : metadataValue) { + if (StringUtils.isBlank(value)) { + continue; + } MetadatumDTO metadatumDto = new MetadatumDTO(); metadatumDto.setValue(value); metadatumDto.setElement(field.getElement()); From 4acaa5983789408b85f272a6e0625457cc6083f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:13:38 +0100 Subject: [PATCH 569/701] build(deps): bump software.amazon.awssdk:s3 in the amazon-s3 group (#12060) Bumps the amazon-s3 group with 1 update: software.amazon.awssdk:s3. Updates `software.amazon.awssdk:s3` from 2.41.33 to 2.41.34 --- updated-dependencies: - dependency-name: software.amazon.awssdk:s3 dependency-version: 2.41.34 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: amazon-s3 ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 972f392a866d..d8c5101fe8bf 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -715,7 +715,7 @@ software.amazon.awssdk s3 - 2.41.33 + 2.42.1 software.amazon.awssdk From 18fb4de65b8fab72286abf785e8dc9ae927a203c Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Wed, 29 Oct 2025 13:52:57 +0100 Subject: [PATCH 570/701] Optimize SQL query in findByEPerson to enhance performance when retrieving associated groups (cherry picked from commit 548c5e18bda6c876d8c313e9b3779de2b1085eb7) --- .../src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java index abd9fc830fa4..5c64af1c5097 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/GroupDAOImpl.java @@ -92,7 +92,7 @@ public List findAll(Context context, int pageSize, int offset) throws SQL @Override public List findByEPerson(Context context, EPerson ePerson) throws SQLException { Query query = createQuery(context, - "from Group where (from EPerson e where e.id = :eperson_id) in elements(epeople)"); + "select distinct g from Group g join g.epeople ep where ep.id = :eperson_id"); query.setParameter("eperson_id", ePerson.getID()); query.setHint("org.hibernate.cacheable", Boolean.TRUE); From 0839934af178fa25cbba15a5b47961c7de149ddb Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Thu, 5 Mar 2026 12:33:58 -0500 Subject: [PATCH 571/701] Replace deprecated internal Base64 with JRE implementation. (cherry picked from commit 20ca87af288d9c8a72c49fd822b7cb6b033e0b98) --- .../epo/service/EpoImportMetadataSourceServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index 4ec1f4db39e7..60ed389383da 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -14,6 +14,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; @@ -29,7 +30,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpException; import org.apache.http.client.utils.URIBuilder; -import org.apache.jena.ext.xerces.impl.dv.util.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.util.XMLUtils; @@ -163,7 +163,7 @@ private Map> getLoginParams() { private Map getLoginHeaderParams() { Map params = new HashMap(); String authString = consumerKey + ":" + consumerSecret; - params.put("Authorization", "Basic " + Base64.encode(authString.getBytes())); + params.put("Authorization", "Basic " + Base64.getEncoder().encodeToString(authString.getBytes())); params.put("Content-type", "application/x-www-form-urlencoded"); return params; } From dfe486a6bc5f4ef062f3dd1245a227c3c38b9926 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 17 Feb 2026 16:42:50 -0600 Subject: [PATCH 572/701] Replace webui.content_disposition_format with webui.content_disposition_inline. The latter is an allowlist instead of a blocklist. --- .../app/rest/BitstreamRestController.java | 65 ++++++++++--------- .../app/rest/BitstreamRestControllerIT.java | 60 ++++++++++------- dspace/config/dspace.cfg | 25 ++++--- 3 files changed, 82 insertions(+), 68 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java index 251bc769cc56..76dca5128032 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamRestController.java @@ -20,7 +20,6 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.core.Response; import org.apache.catalina.connector.ClientAbortException; -import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.converter.ConverterService; @@ -153,15 +152,17 @@ public ResponseEntity retrieve(@PathVariable UUID uuid, HttpServletResponse resp httpHeadersInitializer.withLastModified(lastModified); } - //Determine if we need to send the file as a download or if the browser can open it inline - //The file will be downloaded if its size is larger than the configured threshold, - //or if its mimetype/extension appears in the "webui.content_disposition_format" config - long dispositionThreshold = configurationService.getLongProperty("webui.content_disposition_threshold"); - if ((dispositionThreshold >= 0 && filesize > dispositionThreshold) - || checkFormatForContentDisposition(format)) { - httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT); - } else { + // Determine if we need to send the file as a download or if the browser can open it inline. + // By default, all files will be downloaded as that is more secure. File formats will only be opened inline + // if they are listed in the "webui.content_disposition_inline" config and size is less than the + // configured "webui.content_disposition_threshold" (default = 8MB) + long dispositionThreshold = configurationService.getLongProperty("webui.content_disposition_threshold", + 8388608); + if (checkFormatForContentDispositionInline(format) && + filesize <= dispositionThreshold) { httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_INLINE); + } else { + httpHeadersInitializer.withDisposition(HttpHeadersInitializer.CONTENT_DISPOSITION_ATTACHMENT); } //We have all the data we need, close the connection to the database so that it doesn't stay open during @@ -208,48 +209,48 @@ private boolean isNotAnErrorResponse(HttpServletResponse response) { } /** - * Check if a Bitstream of the specified format should always be downloaded (i.e. "content-disposition: attachment") - * or can be opened inline (i.e. "content-disposition: inline"). + * Check if a Bitstream of the specified format should be opened inline (i.e. "content-disposition: inline") + * instead of the default behavior of always downloading (i.e. "content-disposition: attachment"). *

* NOTE that downloading via "attachment" is more secure, as the user's browser will not attempt to process or * display the file. But, downloading via "inline" may be seen as more user-friendly for common formats. * @param format BitstreamFormat - * @return true if always download ("attachment"). false if can be opened inline ("inline") + * @return true if format is configured to be opened inline ("inline"). false if always download ("attachment") */ - private boolean checkFormatForContentDisposition(BitstreamFormat format) { + private boolean checkFormatForContentDispositionInline(BitstreamFormat format) { // Undefined or Unknown formats should ALWAYS be downloaded for additional security. if (format == null || format.getSupportLevel() == BitstreamFormat.UNKNOWN) { - return true; + return false; } - // Load additional formats configured to require download - List configuredFormats = List.of(configurationService. - getArrayProperty("webui.content_disposition_format")); - - // If configuration includes "*", then all formats will always be downloaded. - if (configuredFormats.contains("*")) { - return true; + // Return false for BANNED inline formats. Some formats, especially XML / HTML / Javascript based formats, + // when loaded inline may be susceptible to XSS attacks. Therefore, we will refuse to allow those formats to be + // displayed inline for security purposes. + // NOTE: "+xml" in this list will match any MIME Type that ends in "+xml", as those are XML-based formats. + List bannedInlineFormats = List.of("text/html", "text/javascript", "text/xml", "application/xml", + "+xml"); + for (String bannedInlineFormat : bannedInlineFormats) { + // If our format MIME Type contains one of the banned inline formats, we refuse to display it inline + if (format.getMIMEType().contains(bannedInlineFormat)) { + return false; + } } - // Define a download list of formats which DSpace forces to ALWAYS be downloaded. - // These formats can embed JavaScript which may be run in the user's browser if the file is opened inline. - // Therefore, DSpace blocks opening these formats inline as it could be used for an XSS attack. - List downloadOnlyFormats = List.of("text/html", "text/javascript", "text/xml", "rdf"); - - // Combine our two lists - List formats = ListUtils.union(downloadOnlyFormats, configuredFormats); + // Load formats configured to allow inline display + List formats = List.of(configurationService. + getArrayProperty("webui.content_disposition_inline")); // See if the passed in format's MIME type or file extension is listed. - boolean download = formats.contains(format.getMIMEType()); - if (!download) { + boolean inline = formats.contains(format.getMIMEType()); + if (!inline) { for (String ext : format.getExtensions()) { if (formats.contains(ext)) { - download = true; + inline = true; break; } } } - return download; + return inline; } /** diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index fe75c9fc518c..1e3a13f46bf5 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -1261,12 +1261,8 @@ public void closeInputStreamsDownloadWithCoverPage() throws Exception { @Test public void checkContentDispositionOfFormats() throws Exception { - configurationService.setProperty("webui.content_disposition_format", new String[] { - "text/richtext", - "text/xml", - "txt" - }); - + // This test verifies that, by default, common text formats will be downloaded instead of being served inline. + // The next two tests will verify behavior of non-default settings of "webui.content_disposition_inline" context.turnOffAuthorisationSystem(); Community community = CommunityBuilder.createCommunity(context).build(); Collection collection = CollectionBuilder.createCollection(context, community).build(); @@ -1288,18 +1284,26 @@ public void checkContentDispositionOfFormats() throws Exception { } context.restoreAuthSystemState(); - // these formats are configured and files should be downloaded + // Based on default configuration all files should be downloaded verifyBitstreamDownload(rtf, "text/richtext;charset=UTF-8", true); verifyBitstreamDownload(xml, "text/xml;charset=UTF-8", true); verifyBitstreamDownload(txt, "text/plain;charset=UTF-8", true); - // this format is not configured and should open inline - verifyBitstreamDownload(csv, "text/csv;charset=UTF-8", false); + verifyBitstreamDownload(csv, "text/csv;charset=UTF-8", true); } @Test - public void checkHardcodedContentDispositionFormats() throws Exception { - // This test is similar to the above test, but it verifies that our *hardcoded settings* for - // webui.content_disposition_format are protecting us from loading specific formats *inline*. + public void checkBannedContentDispositionInlineFormats() throws Exception { + configurationService.setProperty("webui.content_disposition_inline", new String[] { + "text/html", + "text/javascript", + "rdf", + "text/xml", + "image/svg+xml" + }); + + // This test is similar to the above test, but it verifies that if a site specifies + // a banned format (e.g. HTML, XML, etc) in their "webui.content_disposition_inline" setting + // DSpace will still protect them by refusing to load the format *inline*. context.turnOffAuthorisationSystem(); Community community = CommunityBuilder.createCommunity(context).build(); Collection collection = CollectionBuilder.createCollection(context, community).build(); @@ -1329,8 +1333,9 @@ public void checkHardcodedContentDispositionFormats() throws Exception { } context.restoreAuthSystemState(); - // By default, HTML, JS & XML should all download. This protects us from possible XSS attacks, as - // each of these formats can embed JavaScript which may execute when the file is loaded *inline*. + // By default, HTML, JS & XML should all download regardless of inline configuration. + // This protects us from possible XSS attacks, as each of these formats can embed JavaScript + // which may execute when the file is loaded *inline*. verifyBitstreamDownload(html, "text/html;charset=UTF-8", true); verifyBitstreamDownload(js, "text/javascript;charset=UTF-8", true); verifyBitstreamDownload(rdf, "application/rdf+xml;charset=UTF-8", true); @@ -1343,22 +1348,29 @@ public void checkHardcodedContentDispositionFormats() throws Exception { } @Test - public void checkWildcardContentDispositionFormats() throws Exception { - // Setting "*" should result in all formats being downloaded (nothing will be opened inline) - configurationService.setProperty("webui.content_disposition_format", "*"); - + public void checkContentDispositionInlineFormats() throws Exception { + // Set PDF and a few image formats to verify they will display inline. But leave off "text/plain" + configurationService.setProperty("webui.content_disposition_inline", new String[] { + "text/csv", + "application/pdf", + "image/jpeg", + "video/mpeg" + }); context.turnOffAuthorisationSystem(); Community community = CommunityBuilder.createCommunity(context).build(); Collection collection = CollectionBuilder.createCollection(context, community).build(); Item item = ItemBuilder.createItem(context, collection).build(); String content = "Test Content"; Bitstream csv; + Bitstream txt; Bitstream jpg; Bitstream mpg; Bitstream pdf; try (InputStream is = IOUtils.toInputStream(content, CharEncoding.UTF_8)) { csv = BitstreamBuilder.createBitstream(context, item, is) .withMimeType("text/csv").build(); + txt = BitstreamBuilder.createBitstream(context, item, is) + .withMimeType("text/plain").build(); jpg = BitstreamBuilder.createBitstream(context, item, is) .withMimeType("image/jpeg").build(); mpg = BitstreamBuilder.createBitstream(context, item, is) @@ -1368,11 +1380,13 @@ public void checkWildcardContentDispositionFormats() throws Exception { } context.restoreAuthSystemState(); - // All formats should be download only - verifyBitstreamDownload(csv, "text/csv;charset=UTF-8", true); - verifyBitstreamDownload(jpg, "image/jpeg;charset=UTF-8", true); - verifyBitstreamDownload(mpg, "video/mpeg;charset=UTF-8", true); - verifyBitstreamDownload(pdf, "application/pdf;charset=UTF-8", true); + // Only text/plain should download, while other formats should be served inline based on the configuration + verifyBitstreamDownload(csv, "text/csv;charset=UTF-8", false); + verifyBitstreamDownload(jpg, "image/jpeg;charset=UTF-8", false); + verifyBitstreamDownload(mpg, "video/mpeg;charset=UTF-8", false); + verifyBitstreamDownload(pdf, "application/pdf;charset=UTF-8", false); + // This is the only format not listed in the inline configuration, so it will be downloaded + verifyBitstreamDownload(txt, "text/plain;charset=UTF-8", true); } diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 01e168937a99..d9dc7cb6f157 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1390,21 +1390,20 @@ websvc.opensearch.max_num_of_items_per_request = 100 #### Content Inline Disposition Threshold #### # -# Set the max size of a bitstream that can be served inline -# Use -1 to force all bitstream to be served inline -webui.content_disposition_threshold = 8388608 - -#### Content Attachment Disposition Formats #### -# -# Set which mimetypes or file extensions will NOT be opened inline. -# Files with these mimetypes/extensions will always be downloaded, regardless of the threshold above. +# Set which mimetypes or file extensions are allowed to be opened inline in a user's browser. +# By default, all files will be downloaded, regardless of the threshold below, unless specified in this configuration. # NOTE: For security reasons, some file formats (e.g. HTML, XML, RDF, JS) will always be downloaded regardless -# of the settings here. This blocks these formats from executing embedded JavaScript when opened inline. -# For additional security, you may choose to set this to "*" to force all formats to always be downloaded -# (i.e. disables all formats from opening inline within the user's browser). +# of the settings here. This blocks these formats from executing embedded JavaScript when opened inline, protecting +# the site from potential XSS attacks. +# +# For example: enable PDF and common images formats to be opened in a user's browser. +#webui.content_disposition_inline = application/pdf, image/gif, image/jpeg, image/png + # -# By default, RTF is always downloaded because most browsers attempt to display it as plain text. -webui.content_disposition_format = text/richtext +# Set the max size (in bytes) of a bitstream that can be served inline. This setting only applies to formats +# specified in the "webui.content_disposition_inline" configuration above. +# Default = 8MB (8388608 bytes). Use -1 to ignore the size of file when serving it inline. +#webui.content_disposition_threshold = 8388608 #### Multi-file HTML document/site settings ##### # TODO: UNSUPPORTED in DSpace 7.0. May be re-added in a later release From cef4bd1131350d5023f358c91daba8457205b25c Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 13 Mar 2026 13:55:09 -0500 Subject: [PATCH 573/701] Enable inline display by default for common, generally safe formats --- dspace/config/dspace.cfg | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d9dc7cb6f157..53d1ee039ec3 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1396,8 +1396,9 @@ websvc.opensearch.max_num_of_items_per_request = 100 # of the settings here. This blocks these formats from executing embedded JavaScript when opened inline, protecting # the site from potential XSS attacks. # -# For example: enable PDF and common images formats to be opened in a user's browser. -#webui.content_disposition_inline = application/pdf, image/gif, image/jpeg, image/png +# For example: this setting defaults to enabling PDF and common image / audio / video formats to be opened +# (or potentially streamed) in a user's browser. +webui.content_disposition_inline = application/pdf, image/gif, image/jpeg, image/png, audio/mpeg, video/mpeg, video/mp4 # # Set the max size (in bytes) of a bitstream that can be served inline. This setting only applies to formats From 1efc6dc211d65817bb0aad9471b0af7737f6380e Mon Sep 17 00:00:00 2001 From: bram-atmire Date: Wed, 25 Mar 2026 20:21:26 +0100 Subject: [PATCH 574/701] Improve oai.identifier.prefix config documentation Clarify that the value must be a plain hostname (not a full URL) and warn against setting it to ${dspace.ui.url}, which would include the protocol in OAI identifiers. (cherry picked from commit fb508839031e4049be796f76866b73f433d54316) --- dspace/config/modules/oai.cfg | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dspace/config/modules/oai.cfg b/dspace/config/modules/oai.cfg index 98b10f59dee9..87603b1d5da5 100644 --- a/dspace/config/modules/oai.cfg +++ b/dspace/config/modules/oai.cfg @@ -30,9 +30,14 @@ oai.solr.url=${solr.server}/${solr.multicorePrefix}oai # This field is used for two purposes: # 1. As your OAI-PMH # 2. As the prefix for all Identifiers in OAI-PMH (Format is "oai:${oai.identifier.prefix}:${handle.prefix}") -# The OAI-PMH spec requires this prefix to correspond to your site's hostname. Therefore, by default, -# DSpace will set this configuration to the hostname from your ${dspace.ui.url} configuration. -# However, you may override that default value by uncommenting this configuration. +# The OAI-PMH spec requires this prefix to correspond to your site's hostname (without protocol). +# By default, DSpace automatically extracts the hostname from your ${dspace.ui.url} setting +# (e.g. "https://demo.dspace.org" becomes "demo.dspace.org"). +# +# WARNING: Only uncomment this if you need a DIFFERENT hostname than what ${dspace.ui.url} provides. +# The value must be a plain hostname (e.g. "demo.dspace.org"), NOT a full URL. +# Do NOT set this to ${dspace.ui.url} -- that would include the protocol (https://) in OAI identifiers, +# which violates the OAI-PMH specification. # oai.identifier.prefix = YOUR_SITE_HOSTNAME # Base url for bitstreams From 740b6905f95b86b1045293dfc78357b9064db2a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:39:45 +0000 Subject: [PATCH 575/701] build(deps): bump tika.version from 3.2.3 to 3.3.0 Bumps `tika.version` from 3.2.3 to 3.3.0. Updates `org.apache.tika:tika-core` from 3.2.3 to 3.3.0 - [Changelog](https://github.com/apache/tika/blob/main/CHANGES.txt) - [Commits](https://github.com/apache/tika/compare/3.2.3...3.3.0) Updates `org.apache.tika:tika-parsers-standard-package` from 3.2.3 to 3.3.0 --- updated-dependencies: - dependency-name: org.apache.tika:tika-core dependency-version: 3.3.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.apache.tika:tika-parsers-standard-package dependency-version: 3.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6fddbe6c9a7c..64df4c7dcfc2 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 3.0.6 1.19.0 2.0.17 - 3.2.3 + 3.3.0 From 51b4bf5eaa0034ab22cce8b983a38e207986d0c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:58:40 +0000 Subject: [PATCH 576/701] build(deps): bump docker/setup-buildx-action from 3 to 4 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/reusable-docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 98ee15d57f65..e2e7cf06bdd0 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -123,7 +123,7 @@ jobs: # https://github.com/docker/setup-buildx-action - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 # https://github.com/docker/metadata-action # Extract metadata used for Docker images in all build steps below @@ -260,7 +260,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Add Docker metadata for image id: meta From 36cadd8eebef45f720ca55060012157c73c28041 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:58:50 +0000 Subject: [PATCH 577/701] build(deps): bump docker/metadata-action from 5 to 6 Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6. - [Release notes](https://github.com/docker/metadata-action/releases) - [Commits](https://github.com/docker/metadata-action/compare/v5...v6) --- updated-dependencies: - dependency-name: docker/metadata-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/reusable-docker-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 98ee15d57f65..2c4f02cbac5a 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -129,7 +129,7 @@ jobs: # Extract metadata used for Docker images in all build steps below - name: Extract metadata (tags, labels) from GitHub for Docker image id: meta_build - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }} tags: ${{ env.IMAGE_TAGS }} @@ -264,7 +264,7 @@ jobs: - name: Add Docker metadata for image id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.DOCKER_BUILD_REGISTRY }}/${{ env.IMAGE_NAME }} tags: ${{ env.IMAGE_TAGS }} @@ -308,7 +308,7 @@ jobs: # This recreates Docker tags for DockerHub - name: Add Docker metadata for image id: meta_dockerhub - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: ${{ env.IMAGE_NAME }} tags: ${{ env.IMAGE_TAGS }} From 740ce62dd5ff363b57e85daa8e4afff21ec14fd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:59:00 +0000 Subject: [PATCH 578/701] build(deps): bump docker/build-push-action from 6 to 7 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/reusable-docker-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 98ee15d57f65..b5696c1d6dc0 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -147,7 +147,7 @@ jobs: - name: Build and push image to ${{ env.DOCKER_BUILD_REGISTRY }} if: ${{ ! matrix.isPr }} id: docker_build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: build-contexts: | ${{ inputs.dockerfile_additional_contexts }} @@ -201,7 +201,7 @@ jobs: # NOTE: This step cannot be combined with the build above as it's a different type of output. - name: Build and push image to local TAR file if: ${{ matrix.arch == 'linux/amd64'}} - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: build-contexts: | ${{ inputs.dockerfile_additional_contexts }} From 2adf5a662b6486522385f8030cbaff971f5c8aa5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:59:08 +0000 Subject: [PATCH 579/701] build(deps): bump docker/login-action from 3 to 4 Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v4) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/reusable-docker-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 98ee15d57f65..0b5d220f7e6a 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -115,7 +115,7 @@ jobs: # NOTE: This login occurs for BOTH non-PRs or PRs. PRs *must* also login to access private images from GHCR # during the build process - name: Login to ${{ env.DOCKER_BUILD_REGISTRY }} - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.DOCKER_BUILD_REGISTRY }} username: ${{ github.repository_owner }} @@ -253,7 +253,7 @@ jobs: merge-multiple: true - name: Login to ${{ env.DOCKER_BUILD_REGISTRY }} - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.DOCKER_BUILD_REGISTRY }} username: ${{ github.repository_owner }} @@ -316,7 +316,7 @@ jobs: # Login to source registry first, as this is where we are copying *from* - name: Login to ${{ env.DOCKER_BUILD_REGISTRY }} - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.DOCKER_BUILD_REGISTRY }} username: ${{ github.repository_owner }} @@ -324,7 +324,7 @@ jobs: # Login to DockerHub, since this is where we are copying *to* - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_ACCESS_TOKEN }} From 54a0a847270a927dc8c0091fea3e408adf4a2a17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:19:59 +0200 Subject: [PATCH 580/701] build(deps): bump actions/upload-artifact from 4 to 7 (#12238) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/reusable-docker-build.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b66d0d01f48d..b14043c94b89 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,14 +65,14 @@ jobs: # (This artifact is downloadable at the bottom of any job's summary page) - name: Upload Results of ${{ matrix.type }} to Artifact if: ${{ failure() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.type }} results path: ${{ matrix.resultsdir }} # Upload code coverage report to artifact, so that it can be shared with the 'codecov' job (see below) - name: Upload code coverage report to Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.type }} coverage report path: 'dspace/target/site/jacoco-aggregate/jacoco.xml' diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 98ee15d57f65..b37319e4ed50 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -178,7 +178,7 @@ jobs: # (The purpose of the combined manifest is to list both amd64 and arm64 builds under same tag) - name: Upload Docker build digest to artifact if: ${{ ! matrix.isPr }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: digests-${{ inputs.build_id }}-${{ env.ARCH_NAME }} path: /tmp/digests/* @@ -224,7 +224,7 @@ jobs: # This step is only done for AMD64, as that's the only image we use in our automated testing (at this time). - name: Upload local image TAR to artifact if: ${{ matrix.arch == 'linux/amd64'}} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: docker-image-${{ inputs.build_id }}-${{ env.ARCH_NAME }} path: /tmp/${{ inputs.build_id }}.tar From 7edd8eb7ed59e524b1744eb6e635e1c400150f88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:20:49 +0200 Subject: [PATCH 581/701] build(deps): bump the spring group with 25 updates (#12207) Bumps the spring group with 25 updates: | Package | From | To | | --- | --- | --- | | [org.springframework:spring-orm](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-core](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-beans](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-aop](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-context](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-context-support](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-tx](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-jdbc](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-web](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-webmvc](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-expression](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework:spring-test](https://github.com/spring-projects/spring-framework) | `6.2.16` | `6.2.17` | | [org.springframework.boot:spring-boot-starter-test](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-tomcat](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-maven-plugin](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-cache](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-thymeleaf](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-web](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-data-rest](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-security](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-aop](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-actuator](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.boot:spring-boot-starter-log4j2](https://github.com/spring-projects/spring-boot) | `3.5.11` | `3.5.12` | | [org.springframework.security:spring-security-test](https://github.com/spring-projects/spring-security) | `6.5.8` | `6.5.9` | Updates `org.springframework:spring-orm` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-core` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-beans` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-aop` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-context` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-context-support` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-tx` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-jdbc` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-web` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-webmvc` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-expression` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-test` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-core` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-beans` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-aop` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-context` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-context-support` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-tx` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-jdbc` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-web` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-webmvc` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-expression` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework:spring-test` from 6.2.16 to 6.2.17 - [Release notes](https://github.com/spring-projects/spring-framework/releases) - [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.16...v6.2.17) Updates `org.springframework.boot:spring-boot-starter-test` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-tomcat` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.security:spring-security-test` from 6.5.8 to 6.5.9 - [Release notes](https://github.com/spring-projects/spring-security/releases) - [Changelog](https://github.com/spring-projects/spring-security/blob/main/RELEASE.adoc) - [Commits](https://github.com/spring-projects/spring-security/compare/6.5.8...6.5.9) Updates `org.springframework.boot:spring-boot-maven-plugin` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-cache` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-thymeleaf` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-web` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-data-rest` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-security` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-aop` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-actuator` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) Updates `org.springframework.boot:spring-boot-starter-log4j2` from 3.5.11 to 3.5.12 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.11...v3.5.12) --- updated-dependencies: - dependency-name: org.springframework:spring-orm dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-core dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-beans dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-aop dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-context-support dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-tx dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-jdbc dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-web dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-webmvc dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-expression dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework:spring-test dependency-version: 6.2.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-test dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.12 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-tomcat dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.security:spring-security-test dependency-version: 6.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-maven-plugin dependency-version: 3.5.12 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-cache dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-thymeleaf dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-web dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-data-rest dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-security dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-aop dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-actuator dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring - dependency-name: org.springframework.boot:spring-boot-starter-log4j2 dependency-version: 3.5.12 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: spring ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 6fddbe6c9a7c..ea3233f942ef 100644 --- a/pom.xml +++ b/pom.xml @@ -19,10 +19,10 @@ 17 - 6.2.16 + 6.2.17 3.3.6 - 3.5.11 - 6.5.8 + 3.5.12 + 6.5.9 6.4.10.Final 8.0.3.Final 42.7.10 From adbd69d72e2c37241894357c753c5e1f42b1b8d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:21:02 +0200 Subject: [PATCH 582/701] build(deps): bump org.apache.maven.plugins:maven-resources-plugin (#12194) Bumps the build-tools group with 1 update: [org.apache.maven.plugins:maven-resources-plugin](https://github.com/apache/maven-resources-plugin). Updates `org.apache.maven.plugins:maven-resources-plugin` from 3.4.0 to 3.5.0 - [Release notes](https://github.com/apache/maven-resources-plugin/releases) - [Commits](https://github.com/apache/maven-resources-plugin/compare/v3.4.0...maven-resources-plugin-3.5.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-resources-plugin dependency-version: 3.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: build-tools ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ea3233f942ef..6418224c1037 100644 --- a/pom.xml +++ b/pom.xml @@ -356,7 +356,7 @@ org.apache.maven.plugins maven-resources-plugin - 3.4.0 + 3.5.0 From 86670b65157bf7e4cf69ae954571a25196b25e9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:21:18 +0200 Subject: [PATCH 583/701] build(deps): bump commons-logging:commons-logging (#12198) Bumps the apache-commons group with 1 update: [commons-logging:commons-logging](https://github.com/apache/commons-logging). Updates `commons-logging:commons-logging` from 1.3.5 to 1.3.6 - [Changelog](https://github.com/apache/commons-logging/blob/master/RELEASE-NOTES.txt) - [Commits](https://github.com/apache/commons-logging/compare/rel/commons-logging-1.3.5...rel/commons-logging-1.3.6) --- updated-dependencies: - dependency-name: commons-logging:commons-logging dependency-version: 1.3.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: apache-commons ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6418224c1037..e0220e040f50 100644 --- a/pom.xml +++ b/pom.xml @@ -1544,7 +1544,7 @@ commons-logging commons-logging - 1.3.5 + 1.3.6 org.apache.commons From 54f7d74121132eebaf1c88309558e4cbe5d9430a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:21:34 +0200 Subject: [PATCH 584/701] build(deps): bump org.glassfish.jaxb:jaxb-runtime in the jakarta group (#12201) Bumps the jakarta group with 1 update: org.glassfish.jaxb:jaxb-runtime. Updates `org.glassfish.jaxb:jaxb-runtime` from 4.0.6 to 4.0.7 --- updated-dependencies: - dependency-name: org.glassfish.jaxb:jaxb-runtime dependency-version: 4.0.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e0220e040f50..24c7e337f761 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ 2.21 2.1.1 4.0.5 - 4.0.6 + 4.0.7 1.1.1 9.4.58.v20250814 From 819c8d4d6d5f389f2bfc1a6c8b2b37e03edaaa1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:21:47 +0200 Subject: [PATCH 585/701] build(deps): bump the amazon-s3 group with 2 updates (#12197) Bumps the amazon-s3 group with 2 updates: software.amazon.awssdk:s3 and [software.amazon.awssdk.crt:aws-crt](https://github.com/awslabs/aws-crt-java). Updates `software.amazon.awssdk:s3` from 2.42.1 to 2.42.20 Updates `software.amazon.awssdk.crt:aws-crt` from 0.43.4 to 0.43.9 - [Release notes](https://github.com/awslabs/aws-crt-java/releases) - [Commits](https://github.com/awslabs/aws-crt-java/compare/v0.43.4...v0.43.9) --- updated-dependencies: - dependency-name: software.amazon.awssdk:s3 dependency-version: 2.42.20 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: amazon-s3 - dependency-name: software.amazon.awssdk.crt:aws-crt dependency-version: 0.43.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: amazon-s3 ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dspace-api/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d8c5101fe8bf..d21594bcc776 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -715,7 +715,7 @@ software.amazon.awssdk s3 - 2.42.1 + 2.42.20 software.amazon.awssdk @@ -731,7 +731,7 @@ software.amazon.awssdk.crt aws-crt - 0.43.4 + 0.43.9 - 2.21.1 + 2.21.2 2.21 2.1.1 4.0.5 From e29b224bdbca718357edc8e634fe9c29545b75c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:22:23 +0200 Subject: [PATCH 587/701] build(deps-dev): bump org.testcontainers:testcontainers (#12195) Bumps the test-tools group with 1 update: [org.testcontainers:testcontainers](https://github.com/testcontainers/testcontainers-java). Updates `org.testcontainers:testcontainers` from 2.0.3 to 2.0.4 - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/2.0.3...2.0.4) --- updated-dependencies: - dependency-name: org.testcontainers:testcontainers dependency-version: 2.0.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d21594bcc776..e3723dfb22d7 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -862,7 +862,7 @@ org.testcontainers testcontainers - 2.0.3 + 2.0.4 test From 9620ed17589d92e2950af4709439499b5dfb2a19 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 13 Mar 2026 17:09:33 -0500 Subject: [PATCH 588/701] Update all Docker Compose scripts to allow for environment variable overrides from the shell. If override is not provided, the existing default value is still used. --- docker-compose-cli.yml | 4 ++-- docker-compose.yml | 12 ++++++------ .../main/docker-compose/cli.assetstore.yml | 2 +- dspace/src/main/docker-compose/cli.ingest.yml | 6 +++--- .../src/main/docker-compose/db.entities.yml | 2 +- dspace/src/main/docker-compose/db.restore.yml | 2 +- .../docker-compose/docker-compose-angular.yml | 19 ++++++++++--------- 7 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docker-compose-cli.yml b/docker-compose-cli.yml index 564759fcf375..98c8e4d1e0ff 100644 --- a/docker-compose-cli.yml +++ b/docker-compose-cli.yml @@ -17,9 +17,9 @@ services: # __P__ => "." (e.g. dspace__P__dir => dspace.dir) # __D__ => "-" (e.g. google__D__metadata => google-metadata) # db.url: Ensure we are using the 'dspacedb' image for our database - db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' + db__P__url: ${db__P__url:-jdbc:postgresql://dspacedb:5432/dspace} # solr.server: Ensure we are using the 'dspacesolr' image for Solr - solr__P__server: http://dspacesolr:8983/solr + solr__P__server: ${solr__P__server:-http://dspacesolr:8983/solr} volumes: # Keep DSpace assetstore directory between reboots - assetstore:/dspace/assetstore diff --git a/docker-compose.yml b/docker-compose.yml index 016046e4c585..d886d9aeee43 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,16 +22,16 @@ services: # dspace__P__ui__P__url: http://localhost:4000 # Set SSR URL to the Docker container name so that UI can contact container directly in Production mode. # (This is necessary for docker-compose-angular.yml as it uses production mode by default) - dspace__P__server__P__ssr__P__url: http://dspace:8080/server - dspace__P__name: 'DSpace Started with Docker Compose' + dspace__P__server__P__ssr__P__url: ${dspace__P__server__P__ssr__P__url:-http://dspace:8080/server} + dspace__P__name: ${dspace__P__name:-DSpace Started with Docker Compose} # db.url: Ensure we are using the 'dspacedb' image for our database - db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' + db__P__url: ${db__P__url:-jdbc:postgresql://dspacedb:5432/dspace} # solr.server: Ensure we are using the 'dspacesolr' image for Solr - solr__P__server: http://dspacesolr:8983/solr + solr__P__server: ${solr__P__server:-http://dspacesolr:8983/solr} # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. - proxies__P__trusted__P__ipranges: '172.23.0' - LOGGING_CONFIG: /dspace/config/log4j2-container.xml + proxies__P__trusted__P__ipranges: ${proxies__P__trusted__P__ipranges:-172.23.0} + LOGGING_CONFIG: ${LOGGING_CONFIG:-/dspace/config/log4j2-container.xml} image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-8_x-test}" build: context: . diff --git a/dspace/src/main/docker-compose/cli.assetstore.yml b/dspace/src/main/docker-compose/cli.assetstore.yml index 6563aa081eb1..ab9527f8e4af 100644 --- a/dspace/src/main/docker-compose/cli.assetstore.yml +++ b/dspace/src/main/docker-compose/cli.assetstore.yml @@ -10,7 +10,7 @@ services: dspace-cli: environment: # This assetstore zip is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - - LOADASSETS=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz + - LOADASSETS=${LOADASSETS:-https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz} entrypoint: - /bin/bash - '-c' diff --git a/dspace/src/main/docker-compose/cli.ingest.yml b/dspace/src/main/docker-compose/cli.ingest.yml index 6172147f1955..b6b9886eb730 100644 --- a/dspace/src/main/docker-compose/cli.ingest.yml +++ b/dspace/src/main/docker-compose/cli.ingest.yml @@ -9,9 +9,9 @@ services: dspace-cli: environment: - - AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/main/dogAndReport.zip - - ADMIN_EMAIL=test@test.edu - - AIPDIR=/tmp/aip-dir + - AIPZIP=${AIPZIP:-https://github.com/DSpace-Labs/AIP-Files/raw/main/dogAndReport.zip} + - ADMIN_EMAIL=${ADMIN_EMAIL:-test@test.edu} + - AIPDIR=${AIPDIR:-/tmp/aip-dir} entrypoint: - /bin/bash - '-c' diff --git a/dspace/src/main/docker-compose/db.entities.yml b/dspace/src/main/docker-compose/db.entities.yml index 8b09625f6b77..f72fa8eb041c 100644 --- a/dspace/src/main/docker-compose/db.entities.yml +++ b/dspace/src/main/docker-compose/db.entities.yml @@ -11,7 +11,7 @@ services: image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql" environment: # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - - LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql + - LOADSQL=${LOADSQL:-https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql} dspace: ### OVERRIDE default 'entrypoint' in 'docker-compose.yml #### # Ensure that the database is ready BEFORE starting tomcat diff --git a/dspace/src/main/docker-compose/db.restore.yml b/dspace/src/main/docker-compose/db.restore.yml index fa34019a2dc0..b6c826ad5312 100644 --- a/dspace/src/main/docker-compose/db.restore.yml +++ b/dspace/src/main/docker-compose/db.restore.yml @@ -15,7 +15,7 @@ services: image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql" environment: # Location where the dump SQL file will be available on the running container - - LOCALSQL=/tmp/pgdump.sql + - LOCALSQL=${LOCALSQL:-/tmp/pgdump.sql} volumes: # Volume which shares a local SQL file at "./pgdump.sql" to the running container # IF YOUR LOCAL FILE HAS A DIFFERENT NAME (or is in a different location), then change the "./pgdump.sql" diff --git a/dspace/src/main/docker-compose/docker-compose-angular.yml b/dspace/src/main/docker-compose/docker-compose-angular.yml index 0f76be0bba4d..253c37114ffb 100644 --- a/dspace/src/main/docker-compose/docker-compose-angular.yml +++ b/dspace/src/main/docker-compose/docker-compose-angular.yml @@ -18,16 +18,17 @@ services: depends_on: - dspace environment: - DSPACE_UI_SSL: 'false' - DSPACE_UI_HOST: dspace-angular - DSPACE_UI_PORT: '4000' - DSPACE_UI_NAMESPACE: / - DSPACE_REST_SSL: 'false' - DSPACE_REST_HOST: localhost - DSPACE_REST_PORT: 8080 - DSPACE_REST_NAMESPACE: /server + DSPACE_UI_SSL: ${DSPACE_UI_SSL:-false} + DSPACE_UI_HOST: ${DSPACE_UI_HOST:-dspace-angular} + DSPACE_UI_PORT: ${DSPACE_UI_PORT:-4000} + DSPACE_UI_NAMESPACE: ${DSPACE_UI_NAMESPACE:-/} + DSPACE_UI_BASEURL: ${DSPACE_UI_BASEURL:-http://localhost:4000} + DSPACE_REST_SSL: ${DSPACE_REST_SSL:-false} + DSPACE_REST_HOST: ${DSPACE_REST_HOST:-localhost} + DSPACE_REST_PORT: ${DSPACE_REST_PORT:-8080} + DSPACE_REST_NAMESPACE: ${DSPACE_REST_NAMESPACE:-/server} # Ensure SSR can use the 'dspace' Docker image directly (see docker-compose-rest.yml) - DSPACE_REST_SSRBASEURL: http://dspace:8080/server + DSPACE_REST_SSRBASEURL: ${DSPACE_REST_SSRBASEURL:-http://dspace:8080/server} image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x-dist}" ports: - published: 4000 From cbd99ded06fb2f504454c067c540ec56a3f3d91f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:25:15 +0000 Subject: [PATCH 589/701] build(deps): bump the amazon-s3 group with 2 updates Bumps the amazon-s3 group with 2 updates: software.amazon.awssdk:s3 and [software.amazon.awssdk.crt:aws-crt](https://github.com/awslabs/aws-crt-java). Updates `software.amazon.awssdk:s3` from 2.42.20 to 2.42.26 Updates `software.amazon.awssdk.crt:aws-crt` from 0.43.9 to 0.44.0 - [Release notes](https://github.com/awslabs/aws-crt-java/releases) - [Commits](https://github.com/awslabs/aws-crt-java/compare/v0.43.9...v0.44.0) --- updated-dependencies: - dependency-name: software.amazon.awssdk:s3 dependency-version: 2.42.26 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: amazon-s3 - dependency-name: software.amazon.awssdk.crt:aws-crt dependency-version: 0.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: amazon-s3 ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index e3723dfb22d7..31ac0523f814 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -715,7 +715,7 @@ software.amazon.awssdk s3 - 2.42.20 + 2.42.26 software.amazon.awssdk @@ -731,7 +731,7 @@ software.amazon.awssdk.crt aws-crt - 0.43.9 + 0.44.0 6.4.10.Final 8.0.3.Final From 3e910f1b1ccd0f6776b3955ec796f159d5130eef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:28:06 +0000 Subject: [PATCH 591/701] build(deps): bump log4j.version from 2.25.3 to 2.25.4 Bumps `log4j.version` from 2.25.3 to 2.25.4. Updates `org.apache.logging.log4j:log4j-api` from 2.25.3 to 2.25.4 Updates `org.apache.logging.log4j:log4j-core` from 2.25.3 to 2.25.4 Updates `org.apache.logging.log4j:log4j-slf4j2-impl` from 2.25.3 to 2.25.4 --- updated-dependencies: - dependency-name: org.apache.logging.log4j:log4j-api dependency-version: 2.25.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-core dependency-version: 2.25.4 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.apache.logging.log4j:log4j-slf4j2-impl dependency-version: 2.25.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 57009857a455..452ac374a89e 100644 --- a/pom.xml +++ b/pom.xml @@ -40,7 +40,7 @@ 1.1.1 9.4.58.v20250814 - 2.25.3 + 2.25.4 3.0.6 1.19.0 2.0.17 From e51cb00595a09f838c8ec2f59f3e2ff760020d93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:29:27 +0000 Subject: [PATCH 592/701] build(deps): bump org.apache.ant:ant from 1.10.15 to 1.10.16 Bumps org.apache.ant:ant from 1.10.15 to 1.10.16. --- updated-dependencies: - dependency-name: org.apache.ant:ant dependency-version: 1.10.16 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 57009857a455..a22c50ebccf2 100644 --- a/pom.xml +++ b/pom.xml @@ -1413,7 +1413,7 @@ org.apache.ant ant - 1.10.15 + 1.10.16 org.apache.jena From 3d445c1e8838ac65ae8f481ab347b979576eca12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 22:30:12 +0000 Subject: [PATCH 593/701] build(deps): bump org.checkerframework:checker-qual Bumps [org.checkerframework:checker-qual](https://github.com/typetools/checker-framework) from 3.53.1 to 3.54.0. - [Release notes](https://github.com/typetools/checker-framework/releases) - [Changelog](https://github.com/typetools/checker-framework/blob/master/docs/CHANGELOG.md) - [Commits](https://github.com/typetools/checker-framework/compare/checker-framework-3.53.1...checker-framework-3.54.0) --- updated-dependencies: - dependency-name: org.checkerframework:checker-qual dependency-version: 3.54.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 57009857a455..9bdc250bfa24 100644 --- a/pom.xml +++ b/pom.xml @@ -1377,7 +1377,7 @@ org.checkerframework checker-qual - 3.53.1 + 3.54.0 9.4.58.v20250814 2.25.4 - 3.0.6 + 3.0.7 1.19.0 2.0.17 3.2.3 From 96bde966caa0eb88124e86c37300b41b69f80a7c Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Mon, 13 Apr 2026 08:58:33 +0200 Subject: [PATCH 596/701] Fix NPE in ORCID authentication when request is null (cherry picked from commit 4f3ae5f18201c30b2a2b257e144fed83019507b4) --- .../java/org/dspace/authenticate/OrcidAuthenticationBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java b/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java index 590bbf6cf0ef..be2111fad9bc 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java @@ -120,7 +120,7 @@ public String loginPageURL(Context context, HttpServletRequest request, HttpServ @Override public boolean isUsed(Context context, HttpServletRequest request) { - return request.getAttribute(ORCID_AUTH_ATTRIBUTE) != null; + return request != null && request.getAttribute(ORCID_AUTH_ATTRIBUTE) != null; } @Override From c7e4decbefaaa77ebceb53306978c18ba629f27b Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Wed, 27 Aug 2025 09:47:13 -0300 Subject: [PATCH 597/701] fix(#9663): Add configurable limit to CSV metadata import (cherry picked from commit 557c133ff021851ce2515ce76af91860e5f9edeb) --- .../dspace/app/bulkedit/MetadataImport.java | 10 +++ .../dspace/app/bulkedit/MetadataImportIT.java | 74 ++++++++++++++++++- dspace/config/modules/bulkedit.cfg | 7 +- 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java index b12abbc46eb9..166780a5e81a 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java @@ -358,6 +358,16 @@ public List runImport(Context c, boolean change, // Process each change rowCount = 1; + + int maxItems = configurationService.getIntProperty("bulkedit.import.max-items", 1000); + int numItems = toImport.size(); + if (numItems > maxItems && maxItems > 0) { + throw new MetadataImportException( + "Import contains " + numItems + " items, which exceeds the configured " + + "maximum of " + maxItems + ". You can change this limit by setting " + + "'bulkedit.import.max-items' in your local configuration."); + } + for (DSpaceCSVLine line : toImport) { // Resolve target references to other items populateRefAndRowMap(line, line.getID()); diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java index de1dcc91c9a1..158c3f23e6a8 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java @@ -10,6 +10,7 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.fail; +import static org.junit.Assert.assertNotNull; import java.io.BufferedWriter; import java.io.File; @@ -43,6 +44,8 @@ import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.service.ScriptService; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.junit.Before; import org.junit.Test; @@ -54,6 +57,8 @@ public class MetadataImportIT extends AbstractIntegrationTestWithDatabase { = EPersonServiceFactory.getInstance().getEPersonService(); private final RelationshipService relationshipService = ContentServiceFactory.getInstance().getRelationshipService(); + private final ConfigurationService configurationService + = DSpaceServicesFactory.getInstance().getConfigurationService(); private Collection collection; private Collection publicationCollection; @@ -305,4 +310,71 @@ public void performImportScript(String[] csv, boolean useTemplate) throws Except csvFile.delete(); } } -} + + @Test + public void metadataImportExceedsLimitTest() throws Exception { + configurationService.setProperty("bulkedit.import.max-items", 1); + String[] csv = {"id,collection,dc.title", + "+," + collection.getHandle() + ",\"Title 1\"", + "+," + collection.getHandle() + ",\"Title 2\""}; + File csvFile = File.createTempFile("dspace-test-import", "csv"); + try { + try (BufferedWriter out = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream(csvFile), "UTF-8"))) { + for (String csvLine : csv) { + out.write(csvLine + "\n"); + } + } + String fileLocation = csvFile.getAbsolutePath(); + String[] args = new String[] {"metadata-import", "-f", fileLocation, "-e", eperson.getEmail(), "-s"}; + TestDSpaceRunnableHandler testDSpaceRunnableHandler = new TestDSpaceRunnableHandler(); + ScriptLauncher.handleScript( + args, ScriptLauncher.getConfig(kernelImpl), testDSpaceRunnableHandler, kernelImpl); + + assertNotNull("The handler should contain an exception", + testDSpaceRunnableHandler.getException()); + + assertTrue("The exception cause should be a MetadataImportException", + testDSpaceRunnableHandler.getException().getCause() instanceof MetadataImportException); + + String exceptionMessage = testDSpaceRunnableHandler.getException().getCause().getMessage(); + assertTrue("The error message does not contain the expected text.", + exceptionMessage.contains("exceeds the configured maximum of 1")); + } finally { + csvFile.delete(); + } + } + + @Test + public void metadataImportWithItemCountBelowLimitTest() throws Exception { + configurationService.setProperty("bulkedit.import.max-items", 2); + String[] csv = {"id,collection,dc.title", + "+," + collection.getHandle() + ",\"Title 1\"", + "+," + collection.getHandle() + ",\"Title 2\""}; + performImportScript(csv); + Item importedItem1 = findItemByName("Title 1"); + Item importedItem2 = findItemByName("Title 2"); + assertNotNull("Should have imported Title 1", importedItem1); + assertNotNull("Should have imported Title 2", importedItem2); + } + + @Test + public void metadataImportWithLimitDisabledTest() throws Exception { + configurationService.setProperty("bulkedit.import.max-items", 0); + String[] csv = {"id,collection,dc.title", + "+," + collection.getHandle() + ",\"Title 1\"", + "+," + collection.getHandle() + ",\"Title 2\""}; + performImportScript(csv); + Item importedItem1 = findItemByName("Title 1"); + Item importedItem2 = findItemByName("Title 2"); + assertNotNull("Should have imported Title 1 with limit disabled", importedItem1); + assertNotNull("Should have imported Title 2 with limit disabled", importedItem2); + } + + @Test + public void metadataImportWithEmptyCSVTest() throws Exception { + String[] csv = {"id,collection,dc.title"}; + performImportScript(csv); + assertEquals(0, IteratorUtils.toList(itemService.findAll(context)).size()); + } +} \ No newline at end of file diff --git a/dspace/config/modules/bulkedit.cfg b/dspace/config/modules/bulkedit.cfg index e326e007f884..84a12926bc42 100644 --- a/dspace/config/modules/bulkedit.cfg +++ b/dspace/config/modules/bulkedit.cfg @@ -14,10 +14,9 @@ # The delimiter used to serarate authority data (defaults to a double colon ::) # bulkedit.authorityseparator = :: -# A hard limit of the number of items allowed to be edited in one go in the UI -# (does not apply to the command line version) -# TODO: UNSUPPORTED in DSpace 7.0 -# bulkedit.gui-item-limit = 20 +# A hard limit on the number of items allowed to be imported via the UI. +# To disable this limit, set this value to 0. Defaults to 1000. +bulkedit.import.max-items = 1000 # Metadata elements to exclude when exporting via the user interfaces, or when using the # command line version and not using the -a (all) option. From 0ef908dbfd3dac6275bfc003cd8f2e29d5e2ae57 Mon Sep 17 00:00:00 2001 From: JohnnyMendesC <177888064+JohnnyMendesC@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:05:17 -0300 Subject: [PATCH 598/701] refactor: Rename configuration property per review feedback (cherry picked from commit 4da59460955b1045eb038aab8eb86af5c2987eff) --- .../main/java/org/dspace/app/bulkedit/MetadataImport.java | 4 ++-- .../test/java/org/dspace/app/bulkedit/MetadataImportIT.java | 6 +++--- dspace/config/modules/bulkedit.cfg | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java index 166780a5e81a..37a19c6435d2 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java @@ -359,13 +359,13 @@ public List runImport(Context c, boolean change, // Process each change rowCount = 1; - int maxItems = configurationService.getIntProperty("bulkedit.import.max-items", 1000); + int maxItems = configurationService.getIntProperty("bulkedit.import.max.items", 1000); int numItems = toImport.size(); if (numItems > maxItems && maxItems > 0) { throw new MetadataImportException( "Import contains " + numItems + " items, which exceeds the configured " + "maximum of " + maxItems + ". You can change this limit by setting " - + "'bulkedit.import.max-items' in your local configuration."); + + "'bulkedit.import.max.items' in your local configuration."); } for (DSpaceCSVLine line : toImport) { diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java index 158c3f23e6a8..9f28834eff32 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/MetadataImportIT.java @@ -313,7 +313,7 @@ public void performImportScript(String[] csv, boolean useTemplate) throws Except @Test public void metadataImportExceedsLimitTest() throws Exception { - configurationService.setProperty("bulkedit.import.max-items", 1); + configurationService.setProperty("bulkedit.import.max.items", 1); String[] csv = {"id,collection,dc.title", "+," + collection.getHandle() + ",\"Title 1\"", "+," + collection.getHandle() + ",\"Title 2\""}; @@ -347,7 +347,7 @@ public void metadataImportExceedsLimitTest() throws Exception { @Test public void metadataImportWithItemCountBelowLimitTest() throws Exception { - configurationService.setProperty("bulkedit.import.max-items", 2); + configurationService.setProperty("bulkedit.import.max.items", 2); String[] csv = {"id,collection,dc.title", "+," + collection.getHandle() + ",\"Title 1\"", "+," + collection.getHandle() + ",\"Title 2\""}; @@ -360,7 +360,7 @@ public void metadataImportWithItemCountBelowLimitTest() throws Exception { @Test public void metadataImportWithLimitDisabledTest() throws Exception { - configurationService.setProperty("bulkedit.import.max-items", 0); + configurationService.setProperty("bulkedit.import.max.items", 0); String[] csv = {"id,collection,dc.title", "+," + collection.getHandle() + ",\"Title 1\"", "+," + collection.getHandle() + ",\"Title 2\""}; diff --git a/dspace/config/modules/bulkedit.cfg b/dspace/config/modules/bulkedit.cfg index 84a12926bc42..498d051bc6b2 100644 --- a/dspace/config/modules/bulkedit.cfg +++ b/dspace/config/modules/bulkedit.cfg @@ -16,7 +16,7 @@ # A hard limit on the number of items allowed to be imported via the UI. # To disable this limit, set this value to 0. Defaults to 1000. -bulkedit.import.max-items = 1000 +bulkedit.import.max.items = 1000 # Metadata elements to exclude when exporting via the user interfaces, or when using the # command line version and not using the -a (all) option. From ffd7fb5b2ac31829654aae2426ddf17689c920eb Mon Sep 17 00:00:00 2001 From: Nathan Buckingham Date: Mon, 25 Aug 2025 21:21:36 -0400 Subject: [PATCH 599/701] 133543: DSpaceCSV respect metadata.hide properties (cherry picked from commit ff372aa0b97ddecdd927416169640e38b2e754e5) --- .../org/dspace/app/bulkedit/DSpaceCSV.java | 50 ++++++--- .../app/util/MetadataExposureServiceImpl.java | 2 +- .../{DSpaceCSVTest.java => DSpaceCSVIT.java} | 104 +++++++++++++++++- dspace/config/modules/bulkedit.cfg | 3 + 4 files changed, 141 insertions(+), 18 deletions(-) rename dspace-api/src/test/java/org/dspace/app/bulkedit/{DSpaceCSVTest.java => DSpaceCSVIT.java} (65%) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/DSpaceCSV.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/DSpaceCSV.java index 3533a2397b3d..89caa6c15286 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/DSpaceCSV.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/DSpaceCSV.java @@ -25,6 +25,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.util.MetadataExposureServiceImpl; import org.dspace.authority.AuthorityValue; import org.dspace.authority.factory.AuthorityServiceFactory; import org.dspace.authority.service.AuthorityValueService; @@ -321,20 +322,7 @@ protected void init() { // Set the metadata fields to ignore ignore = new HashMap<>(); - // Specify default values - String[] defaultValues = - new String[] { - "dc.date.accessioned", "dc.date.available", "dc.date.updated", "dc.description.provenance" - }; - String[] toIgnoreArray = - DSpaceServicesFactory.getInstance() - .getConfigurationService() - .getArrayProperty("bulkedit.ignore-on-export", defaultValues); - for (String toIgnoreString : toIgnoreArray) { - if (!"".equals(toIgnoreString.trim())) { - ignore.put(toIgnoreString.trim(), toIgnoreString.trim()); - } - } + getConfiguredIgnoreFields(); } /** @@ -352,6 +340,40 @@ public boolean hasActions() { return false; } + /** + * Sets the ignored fields with 'bulkedit.ignore-on-export' + * + * Also adds 'metadata.hide.*' fields to ignored if 'bulkedit.ignore-on-export.include-metadata-hide' is true + */ + private void getConfiguredIgnoreFields() { + // Specify default values + String[] defaultValues = + new String[] { + "dc.date.accessioned", "dc.date.available", "dc.date.updated", "dc.description.provenance" + }; + String[] toIgnoreArray = + DSpaceServicesFactory.getInstance() + .getConfigurationService() + .getArrayProperty("bulkedit.ignore-on-export", defaultValues); + + boolean ignoreHiddenMetadata = DSpaceServicesFactory.getInstance().getConfigurationService() + .getBooleanProperty("bulkedit.ignore-on-export.include-metadata-hide", true); + if (ignoreHiddenMetadata) { + List hiddenMetadata = DSpaceServicesFactory.getInstance().getConfigurationService() + .getPropertyKeys(MetadataExposureServiceImpl.CONFIG_PREFIX); + for (String hiddenMetadataKey : hiddenMetadata) { + String key = hiddenMetadataKey.split(MetadataExposureServiceImpl.CONFIG_PREFIX)[1]; + ignore.put(key.trim(), key.trim()); + } + } + + for (String toIgnoreString : toIgnoreArray) { + if (!"".equals(toIgnoreString.trim())) { + ignore.put(toIgnoreString.trim(), toIgnoreString.trim()); + } + } + } + /** * Set the value separator for multiple values stored in one csv value. * diff --git a/dspace-api/src/main/java/org/dspace/app/util/MetadataExposureServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/util/MetadataExposureServiceImpl.java index 681867371a06..9a281d65775b 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/MetadataExposureServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/MetadataExposureServiceImpl.java @@ -63,7 +63,7 @@ public class MetadataExposureServiceImpl implements MetadataExposureService { protected Map> hiddenElementSets = null; protected Map>> hiddenElementMaps = null; - protected final String CONFIG_PREFIX = "metadata.hide."; + public static final String CONFIG_PREFIX = "metadata.hide."; @Autowired(required = true) protected AuthorizeService authorizeService; diff --git a/dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVTest.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVIT.java similarity index 65% rename from dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVTest.java rename to dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVIT.java index 21a1a67dde2b..f4e1e7f2892f 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVTest.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/DSpaceCSVIT.java @@ -9,6 +9,9 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import java.io.BufferedWriter; @@ -20,7 +23,15 @@ import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.Logger; -import org.dspace.AbstractUnitTest; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Before; import org.junit.Test; @@ -29,11 +40,39 @@ * * @author Stuart Lewis */ -public class DSpaceCSVTest extends AbstractUnitTest { +public class DSpaceCSVIT extends AbstractIntegrationTestWithDatabase { /** * log4j category */ - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceCSVTest.class); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(DSpaceCSVIT.class); + + ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + Item testItem; + + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + configurationService.addPropertyValue("metadata.hide.dc.subject", true); + + Collection parentCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Parent Collection") + .build(); + + testItem = ItemBuilder.createItem(context, parentCollection).withTitle("Test Item") + .withMetadata("dc", "description", "provenance", "provenance") + .withMetadata("dc", "subject", null, "hidden subject") + .build(); + + context.restoreAuthSystemState(); + } /** * Test the reading and parsing of CSV files @@ -147,4 +186,63 @@ public void testDSpaceCSV() { fail("IO Error while creating test CSV file"); } } + + /** + * Test the hidden metadata for csv is respected + * + */ + @Test + public void testHiddenDspaceCSV() throws Exception { + + DSpaceCSV dSpaceCSV = new DSpaceCSV(false); + + dSpaceCSV.addItem(testItem); + + List lines = dSpaceCSV.getCSVLines(); + + assertThat(lines.size(), equalTo(1)); + + DSpaceCSVLine line = lines.get(0); + + List subject = line.get("dc.subject"); + List provenance = line.get("dc.description.provenance"); + List title = line.get("dc.title"); + + assertNull(subject); + assertNull(provenance); + assertNotNull(title); + assertEquals("Test Item", title.get(0)); + + } + + /** + * Test the hidden metadata is still shown when force is applied. + * + */ + @Test + public void testHiddenDspaceForceCSV() throws Exception { + + DSpaceCSV dSpaceCSV = new DSpaceCSV(true); + + dSpaceCSV.addItem(testItem); + + List lines = dSpaceCSV.getCSVLines(); + + assertThat(lines.size(), equalTo(1)); + + DSpaceCSVLine line = lines.get(0); + + List subject = line.get("dc.subject"); + List provenance = line.get("dc.description.provenance"); + List title = line.get("dc.title"); + + assertNotNull(subject); + assertNotNull(provenance); + assertEquals("hidden subject", subject.get(0)); + assertEquals("provenance", provenance.get(0)); + assertNotNull(title); + assertEquals("Test Item", title.get(0)); + + } + } diff --git a/dspace/config/modules/bulkedit.cfg b/dspace/config/modules/bulkedit.cfg index 498d051bc6b2..7a9685d09ace 100644 --- a/dspace/config/modules/bulkedit.cfg +++ b/dspace/config/modules/bulkedit.cfg @@ -44,3 +44,6 @@ bulkedit.change.commit.count = 100 # Recommend to keep this at a feasible number, as exporting large amounts of items can be resource intensive # If not set, this will default to 500 items # bulkedit.export.max.items = 500 + +# Bulkedit setting to add metadata.hide fields to ignored fields defaults to true +# bulkedit.ignore-on-export.include-metadata-hide = true From 953d437ff895de31d77447cee23995b4d3e4cb3c Mon Sep 17 00:00:00 2001 From: Pascal-Nicolas Becker Date: Thu, 11 Dec 2025 12:11:54 +0100 Subject: [PATCH 600/701] Curator does not support items without handles The curation task CreateMissingIdentifiers should create all identifiers an item misses. Unfortunately it will not be started, if the item does not have a handle, as the curator falls back to handles, even if a uuid is given as identifier. This PR changes the Curator to check if an identifier is a uuid if no handle was found. I added a test to the CreateMissingIdentifiersIT. It would probably be better to add a test to the Curator, but the CreateMissingIdentifier task seems to be the most likely target for such a scenario. (cherry picked from commit 4a23b71758cf8d2fd3d0c1c3718f30b13d3452d1) --- .../dspace/curate/AbstractCurationTask.java | 26 +++++++- .../main/java/org/dspace/curate/Curator.java | 18 ++++++ .../general/CreateMissingIdentifiersIT.java | 60 +++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java b/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java index fa16d2736953..ca370bc68fe9 100644 --- a/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java +++ b/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java @@ -11,8 +11,13 @@ import java.sql.SQLException; import java.util.Iterator; import java.util.List; +import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.util.factory.UtilServiceFactory; +import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; @@ -42,6 +47,8 @@ public abstract class AbstractCurationTask implements CurationTask { protected ItemService itemService; protected HandleService handleService; protected ConfigurationService configurationService; + protected DSpaceObjectUtils dspaceObjectUtils; + private static final Logger log = LogManager.getLogger(); @Override public void init(Curator curator, String taskId) throws IOException { @@ -51,6 +58,7 @@ public void init(Curator curator, String taskId) throws IOException { itemService = ContentServiceFactory.getInstance().getItemService(); handleService = HandleServiceFactory.getInstance().getHandleService(); configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + dspaceObjectUtils = UtilServiceFactory.getInstance().getDSpaceObjectUtils(); } @Override @@ -167,8 +175,24 @@ public int perform(Context ctx, String id) throws IOException { * @throws IOException if IO error */ protected DSpaceObject dereference(Context ctx, String id) throws IOException { + // the identifier can be a handle or uuid. Try handle first try { - return handleService.resolveToObject(ctx, id); + DSpaceObject dso = handleService.resolveToObject(ctx, id); + // if the id did not resolve to a handle, check if it is a uuid + if (dso == null) { + UUID uuid = null; + try { + uuid = UUID.fromString(id); + } catch (IllegalArgumentException iae) { + // no uuid, nothing to do here. + log.debug("ID {} is not a valid UUID", id); + } + if (uuid != null) { + dso = dspaceObjectUtils.findDSpaceObject(ctx, uuid); + } + } + // dso is either null or a DSpaceObject + return dso; } catch (SQLException sqlE) { throw new IOException(sqlE.getMessage(), sqlE); } diff --git a/dspace-api/src/main/java/org/dspace/curate/Curator.java b/dspace-api/src/main/java/org/dspace/curate/Curator.java index 4076fab51989..126f07aed229 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curator.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curator.java @@ -15,9 +15,12 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.util.factory.UtilServiceFactory; +import org.dspace.app.util.service.DSpaceObjectUtils; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; @@ -90,6 +93,7 @@ public static enum TxScope { protected TaskResolver resolver = new TaskResolver(); protected TxScope txScope = TxScope.OPEN; protected CommunityService communityService; + protected DSpaceObjectUtils dspaceObjectUtils; protected ItemService itemService; protected HandleService handleService; protected DSpaceRunnableHandler handler; @@ -109,6 +113,7 @@ public Curator(DSpaceRunnableHandler handler) { */ public Curator() { communityService = ContentServiceFactory.getInstance().getCommunityService(); + dspaceObjectUtils = UtilServiceFactory.getInstance().getDSpaceObjectUtils(); itemService = ContentServiceFactory.getInstance().getItemService(); handleService = HandleServiceFactory.getInstance().getHandleService(); resolver = new TaskResolver(); @@ -249,6 +254,19 @@ public void curate(Context c, String id) throws IOException { curationCtx.set(c); DSpaceObject dso = handleService.resolveToObject(c, id); + // check if the DSpace Object does not have a handle and must be resolved by its uuid + if (dso == null) { + UUID uuid = null; + try { + uuid = UUID.fromString(id); + } catch (IllegalArgumentException iae) { + // no uuid, nothing to do here. + log.debug("ID {} is not a valid UUID", id); + } + if (uuid != null) { + dso = dspaceObjectUtils.findDSpaceObject(c, uuid); + } + } if (dso != null) { curate(dso); } else { diff --git a/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java b/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java index 8ecddc6cd50c..63157301b960 100644 --- a/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java +++ b/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java @@ -8,8 +8,12 @@ package org.dspace.ctask.general; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; @@ -19,6 +23,7 @@ import org.dspace.core.factory.CoreServiceFactory; import org.dspace.curate.Curator; import org.dspace.identifier.AbstractIdentifierProviderIT; +import org.dspace.identifier.IdentifierProvider; import org.dspace.identifier.VersionedHandleIdentifierProvider; import org.dspace.identifier.VersionedHandleIdentifierProviderWithCanonicalHandles; import org.dspace.services.ConfigurationService; @@ -85,4 +90,59 @@ public void testPerform() assertEquals("Curation should succeed", Curator.CURATE_SUCCESS, status); configurationService.setProperty(P_TASK_DEF, prevTaskDef); } + + @Test + public void testCreationOfMissingHandles() throws IOException { + // Must remove any cached named plugins before creating a new one + CoreServiceFactory.getInstance().getPluginService().clearNamedPluginClasses(); + // Define a new task dynamically + configurationService.setProperty(P_TASK_DEF, + CreateMissingIdentifiers.class.getCanonicalName() + " = " + TASK_NAME); + + // deactivate all identifier provider + List identifierProviders = identifierService.getProviders(); + List identifierProviderClasses = + identifierProviders.stream().map(Object::getClass).distinct().collect(Collectors.toList()); + for (Class identifierProviderClass : identifierProviderClasses) { + unregisterProvider(identifierProviderClass); + } + + try { + context.setCurrentUser(admin); + parentCommunity = CommunityBuilder.createCommunity(context) + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .build(); + // create item and assert it did not got any handle + Item item = ItemBuilder.createItem(context, collection) + .build(); + assertNull("Internal error in createMissingIdentifiersIT: item should not have a handle", + item.getHandle()); + + // setup the curator + Curator curator = new Curator(); + curator.addTask(TASK_NAME); + + // register the default Handle Provider + registerProvider(VersionedHandleIdentifierProvider.class); + + /* + * Now, verify curate with default Handle Provider works + * (and that our re-registration of the default provider above was successful) + * Use the uuid as reference to the item as the curation system takes handles and uuid as cli arguments. + * Do not use the item reference, to be sure the curator and the curation task are able to work without + * handle. + */ + curator.curate(context, item.getID().toString()); + int status = curator.getStatus(TASK_NAME); + assertEquals("Curation should succeed", Curator.CURATE_SUCCESS, status); + // assure we got a handle + assertNotNull("Curation task CreateMissingIdentifiers, did not assign a handle.", item.getHandle()); + } finally { + // restore the identifierProviders for following tests + for (Class identifierProviderClass : identifierProviderClasses) { + registerProvider(identifierProviderClass); + } + } + } } From de6247db2a04490d06a1ec30a5098b930ead5751 Mon Sep 17 00:00:00 2001 From: Pascal-Nicolas Becker Date: Thu, 19 Feb 2026 16:47:24 +0100 Subject: [PATCH 601/701] Move code to find items by uuid or handle to DSpaceObjectUtil (cherry picked from commit bdc575ff2b65a632b10db463a7e2a1aca6e41828) --- .../app/util/DSpaceObjectUtilsImpl.java | 31 ++++++++++++++++ .../app/util/service/DSpaceObjectUtils.java | 13 +++++++ .../dspace/curate/AbstractCurationTask.java | 35 ++----------------- .../main/java/org/dspace/curate/Curator.java | 16 +-------- 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java b/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java index e3f2b0ea5faa..33621abd529d 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DSpaceObjectUtilsImpl.java @@ -15,12 +15,15 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; +import org.dspace.handle.service.HandleService; import org.springframework.beans.factory.annotation.Autowired; public class DSpaceObjectUtilsImpl implements DSpaceObjectUtils { @Autowired private ContentServiceFactory contentServiceFactory; + @Autowired + private HandleService handleService; /** * Retrieve a DSpaceObject from its uuid. As this method need to iterate over all the different services that @@ -44,4 +47,32 @@ public DSpaceObject findDSpaceObject(Context context, UUID uuid) throws SQLExcep } return null; } + + /** + * Retrieve a DSpaceObject from its uuid or handle. As this method need to iterate over all the different services + * that support concrete class of DSpaceObject it has poor performance. Please consider the use of the direct + * service (ItemService, CommunityService, etc.) if you know in advance the type of DSpaceObject that you are + * looking for + * + * @param context DSpace context + * @param id the uuid or handle to lookup + * @return the DSpaceObject if any with the supplied uuid or handle + * @throws SQLException + */ + public DSpaceObject findDSpaceObject(Context context, String id) throws SQLException { + DSpaceObject dso = handleService.resolveToObject(context, id); + // if the id did not resolve to a handle, check if it is a uuid + if (dso == null) { + UUID uuid = null; + try { + uuid = UUID.fromString(id); + } catch (IllegalArgumentException iae) { + // nothing to do here. We check later fo empty uuids anyway + } + if (uuid != null) { + dso = findDSpaceObject(context, uuid); + } + } + return dso; + } } diff --git a/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java b/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java index e6a97004ef60..8088a4ca4de5 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java +++ b/dspace-api/src/main/java/org/dspace/app/util/service/DSpaceObjectUtils.java @@ -30,4 +30,17 @@ public interface DSpaceObjectUtils { * @throws SQLException */ public DSpaceObject findDSpaceObject(Context context, UUID uuid) throws SQLException; + + /** + * Retrieve a DSpaceObject from its uuid or handle. As this method need to iterate over all the different services + * that support concrete class of DSpaceObject it has poor performance. Please consider the use of the direct + * service (ItemService, CommunityService, etc.) if you know in advance the type of DSpaceObject that you are + * looking for + * + * @param context DSpace context + * @param id the uuid or handle to lookup + * @return the DSpaceObject if any with the supplied uuid or handle + * @throws SQLException + */ + public DSpaceObject findDSpaceObject(Context context, String id) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java b/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java index ca370bc68fe9..4d59d46a8042 100644 --- a/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java +++ b/dspace-api/src/main/java/org/dspace/curate/AbstractCurationTask.java @@ -11,7 +11,6 @@ import java.sql.SQLException; import java.util.Iterator; import java.util.List; -import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -161,41 +160,13 @@ protected void performItem(Item item) throws SQLException, IOException { @Override public int perform(Context ctx, String id) throws IOException { - DSpaceObject dso = dereference(ctx, id); - return (dso != null) ? perform(dso) : Curator.CURATE_FAIL; - } - - /** - * Returns a DSpaceObject for passed identifier, if it exists - * - * @param ctx DSpace context - * @param id canonical id of object - * @return dso - * DSpace object, or null if no object with id exists - * @throws IOException if IO error - */ - protected DSpaceObject dereference(Context ctx, String id) throws IOException { - // the identifier can be a handle or uuid. Try handle first + DSpaceObject dso = null; try { - DSpaceObject dso = handleService.resolveToObject(ctx, id); - // if the id did not resolve to a handle, check if it is a uuid - if (dso == null) { - UUID uuid = null; - try { - uuid = UUID.fromString(id); - } catch (IllegalArgumentException iae) { - // no uuid, nothing to do here. - log.debug("ID {} is not a valid UUID", id); - } - if (uuid != null) { - dso = dspaceObjectUtils.findDSpaceObject(ctx, uuid); - } - } - // dso is either null or a DSpaceObject - return dso; + dso = dspaceObjectUtils.findDSpaceObject(ctx, id); } catch (SQLException sqlE) { throw new IOException(sqlE.getMessage(), sqlE); } + return (dso != null) ? perform(dso) : Curator.CURATE_FAIL; } /** diff --git a/dspace-api/src/main/java/org/dspace/curate/Curator.java b/dspace-api/src/main/java/org/dspace/curate/Curator.java index 126f07aed229..91f984b72e5b 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curator.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curator.java @@ -15,7 +15,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -253,20 +252,7 @@ public void curate(Context c, String id) throws IOException { //Save the context on current execution thread curationCtx.set(c); - DSpaceObject dso = handleService.resolveToObject(c, id); - // check if the DSpace Object does not have a handle and must be resolved by its uuid - if (dso == null) { - UUID uuid = null; - try { - uuid = UUID.fromString(id); - } catch (IllegalArgumentException iae) { - // no uuid, nothing to do here. - log.debug("ID {} is not a valid UUID", id); - } - if (uuid != null) { - dso = dspaceObjectUtils.findDSpaceObject(c, uuid); - } - } + DSpaceObject dso = dspaceObjectUtils.findDSpaceObject(c,id); if (dso != null) { curate(dso); } else { From 45789ea6c8d00d076e3080a804cafa40aeb29c91 Mon Sep 17 00:00:00 2001 From: Pascal-Nicolas Becker Date: Thu, 19 Feb 2026 16:47:53 +0100 Subject: [PATCH 602/701] Restore curation task configuration at the end of IT (cherry picked from commit 0cc9a4d89add6adba4fff2ae4e372b61e0197e2e) --- .../org/dspace/ctask/general/CreateMissingIdentifiersIT.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java b/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java index 63157301b960..5013f2cb6690 100644 --- a/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java +++ b/dspace-api/src/test/java/org/dspace/ctask/general/CreateMissingIdentifiersIT.java @@ -96,6 +96,7 @@ public void testCreationOfMissingHandles() throws IOException { // Must remove any cached named plugins before creating a new one CoreServiceFactory.getInstance().getPluginService().clearNamedPluginClasses(); // Define a new task dynamically + String[] prevTaskDef = configurationService.getArrayProperty(P_TASK_DEF); configurationService.setProperty(P_TASK_DEF, CreateMissingIdentifiers.class.getCanonicalName() + " = " + TASK_NAME); @@ -143,6 +144,8 @@ public void testCreationOfMissingHandles() throws IOException { for (Class identifierProviderClass : identifierProviderClasses) { registerProvider(identifierProviderClass); } + // restore curation task configuration + configurationService.setProperty(P_TASK_DEF, prevTaskDef); } } } From 480a63d890b0d88263c76e9c230f0c834ef21816 Mon Sep 17 00:00:00 2001 From: Mattia Vianelli Date: Mon, 6 Oct 2025 15:58:38 +0200 Subject: [PATCH 603/701] DURACOM-393 Exposed base url for orcid domain (cherry picked from commit a51813e7ec283bedd4e1db727259223daa8992f5) --- dspace/config/modules/rest.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 4c179d5d8900..0a448cfc7ae7 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -60,3 +60,4 @@ rest.properties.exposed = handle.canonical.prefix rest.properties.exposed = contentreport.enable rest.properties.exposed = duplicate.enable rest.properties.exposed = bulkedit.export.max.items +rest.properties.exposed = orcid.domain-url From 5b0fc648457f1dd5b33273b05b9b30e1ee9472aa Mon Sep 17 00:00:00 2001 From: bram-atmire Date: Fri, 13 Mar 2026 15:49:43 +0100 Subject: [PATCH 604/701] Fix race condition in LegacyPluginServiceImpl named plugin cache The namedPluginClasses HashMap was accessed concurrently without synchronization, causing spurious "Name collision in named plugin" errors and potential HashMap corruption under concurrent load. - Replace HashMap with ConcurrentHashMap for thread-safe reads - Add synchronized to configureNamedPlugin() to prevent double init - Use putIfAbsent() in installNamedConfigs() to eliminate check-then-put race Fixes: https://github.com/DSpace/DSpace/issues/12117 Co-Authored-By: Claude Opus 4.6 (cherry picked from commit 7bacc317d669f24e637dab624b1019d2b5a04a1b) --- .../java/org/dspace/core/LegacyPluginServiceImpl.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java index e92ea137f31f..e7c092e75c93 100644 --- a/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/core/LegacyPluginServiceImpl.java @@ -17,6 +17,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -219,10 +220,10 @@ private Object getAnonymousPlugin(String classname) // Map of named plugin classes, [intfc,name] -> class // Also contains intfc -> "marker" to mark when interface has been loaded. - private final Map namedPluginClasses = new HashMap<>(); + private final Map namedPluginClasses = new ConcurrentHashMap<>(); // load and cache configuration data for the given interface. - private void configureNamedPlugin(String iname) + private synchronized void configureNamedPlugin(String iname) throws ClassNotFoundException { int found = 0; @@ -307,11 +308,10 @@ private int installNamedConfigs(String iname, String classname, String names[]) int found = 0; for (int i = 0; i < names.length; ++i) { String key = iname + SEP + names[i]; - if (namedPluginClasses.containsKey(key)) { + String existing = namedPluginClasses.putIfAbsent(key, classname); + if (existing != null) { log.error("Name collision in named plugin, implementation class=\"" + classname + "\", name=\"" + names[i] + "\""); - } else { - namedPluginClasses.put(key, classname); } log.debug("Got Named Plugin, intfc=" + iname + ", name=" + names[i] + ", class=" + classname); ++found; From da7d0b3b0befdb5ea1f249976ee1b02d4912c86d Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 17 Apr 2026 16:32:32 -0500 Subject: [PATCH 605/701] Ensure docker-deploy waits for the backend to fully initialize before creating test content --- .github/workflows/docker.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b3f67ad8e479..5732a22d8277 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -205,8 +205,11 @@ jobs: sleep 10 docker container ls # Create a test admin account. Load test data from a simple set of AIPs as defined in cli.ingest.yml + # NOTE: Before creating test data, we wait for the backend to become responsive by requesting it every 10 sec. + # Timeout after 5 minutes. This is done to ensure the backend is fully initialized before we create test data. - name: Load test data into Backend run: | + timeout 5m wget --retry-connrefused -t 0 --waitretry=10 http://127.0.0.1:8080/server/api docker compose -f docker-compose-cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en docker compose -f docker-compose-cli.yml -f dspace/src/main/docker-compose/cli.ingest.yml run --rm dspace-cli # Verify backend started successfully. From 2e89ab17c3d43669d5ffc8bbb158eb865a09f2aa Mon Sep 17 00:00:00 2001 From: bram-atmire Date: Sat, 18 Apr 2026 09:53:27 +0200 Subject: [PATCH 606/701] Contain IT teardown failures so one flaky cleanup doesn't cascade Wrap each builder's cleanup() in its own try/catch in AbstractBuilderCleanupUtil so a single failure (e.g. the intermittent Hibernate ConcurrentModificationException in ResourceRegistryStandardImpl.releaseResources) no longer aborts cleanup of the remaining builders. The first failure is rethrown with any subsequent failures attached via addSuppressed. Restructure AbstractIntegrationTestWithDatabase.destroy() to run the shared-state resets (Solr cores, authority cache, QA events, config service, builder cache) in a finally block, so that state is always reset between tests even when builder cleanup throws. Previously those resets sat after the try/catch, so any teardown exception would skip them and poison every subsequent test in the class. Fixes #12324 (cherry picked from commit 4007e7ecda176484b8672ac89bb3e9e38f43e71e) --- .../AbstractIntegrationTestWithDatabase.java | 29 +++++++++++++++++-- .../util/AbstractBuilderCleanupUtil.java | 27 +++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java index 76b3fe131be0..8c03e7f1a204 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java +++ b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java @@ -179,15 +179,40 @@ public void setUp() throws Exception { */ @After public void destroy() throws Exception { - // Cleanup our global context object + // Contain the blast radius of teardown failures: the shared static state (Solr cores, authority + // cache, configuration, builder cache) must be reset regardless of whether builder cleanup threw. + // Otherwise a single flake in one test's teardown poisons every subsequent test in the class. + Exception primaryFailure = null; try { AbstractBuilder.cleanupObjects(); parentCommunity = null; cleanupContext(); } catch (Exception e) { - throw new RuntimeException("Error cleaning up builder objects & context object", e); + primaryFailure = new RuntimeException("Error cleaning up builder objects & context object", e); + } finally { + try { + resetSharedState(); + } catch (Exception e) { + if (primaryFailure == null) { + primaryFailure = e; + } else { + primaryFailure.addSuppressed(e); + } + } } + if (primaryFailure != null) { + throw primaryFailure; + } + } + /** + * Reset all shared static state between tests: Solr cores, authority cache, QA events, configuration + * service, and the builder cache. Called from {@link #destroy()} inside a finally block so this always + * runs, even if earlier teardown steps failed. + * + * @throws Exception if reloading configuration or resetting the builder cache fails + */ + private void resetSharedState() throws Exception { ServiceManager serviceManager = DSpaceServicesFactory.getInstance().getServiceManager(); // Clear the search core. diff --git a/dspace-api/src/test/java/org/dspace/builder/util/AbstractBuilderCleanupUtil.java b/dspace-api/src/test/java/org/dspace/builder/util/AbstractBuilderCleanupUtil.java index 6a8daa432eb6..fd5c504a717f 100644 --- a/dspace-api/src/test/java/org/dspace/builder/util/AbstractBuilderCleanupUtil.java +++ b/dspace-api/src/test/java/org/dspace/builder/util/AbstractBuilderCleanupUtil.java @@ -12,6 +12,8 @@ import java.util.List; import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.builder.AbstractBuilder; import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.BitstreamFormatBuilder; @@ -45,6 +47,8 @@ */ public class AbstractBuilderCleanupUtil { + private static final Logger log = LogManager.getLogger(AbstractBuilderCleanupUtil.class); + private final LinkedHashMap> map = new LinkedHashMap<>(); @@ -98,15 +102,34 @@ public void addToMap(AbstractBuilder abstractBuilder) { /** * This method takes care of iterating over all the AbstractBuilders in the predefined order and calls * the cleanup method to delete the objects from the database. - * @throws Exception If something goes wrong + *

+ * Each builder is cleaned up inside its own try/catch so that a single failure (e.g. the intermittent + * Hibernate {@link java.util.ConcurrentModificationException} in resource registry cleanup) does not + * abort cleanup of the remaining builders. The first failure is rethrown at the end with any subsequent + * failures attached as suppressed exceptions, so nothing is silently swallowed. + *

+ * @throws Exception If one or more builders fail to clean up */ public void cleanupBuilders() throws Exception { + Exception firstFailure = null; for (Map.Entry> entry : map.entrySet()) { List list = entry.getValue(); for (AbstractBuilder abstractBuilder : list) { - abstractBuilder.cleanup(); + try { + abstractBuilder.cleanup(); + } catch (Exception e) { + log.error("Error cleaning up builder {}", abstractBuilder.getClass().getName(), e); + if (firstFailure == null) { + firstFailure = e; + } else { + firstFailure.addSuppressed(e); + } + } } } + if (firstFailure != null) { + throw firstFailure; + } } /** From 488b4018c4ac8a7d6bc0410542e283a66397023d Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Tue, 21 Apr 2026 09:41:06 -0500 Subject: [PATCH 607/701] Merge pull request #11967 from dataquest-dev/fix-openaire-integration Fix OpenAIRE integration: null handling and HTTP client lifecycle (cherry picked from commit 3b023ebceff2b1cc6d4fe6af1955cf0964463962) --- checkstyle-suppressions.xml | 1 + .../external/OpenaireRestConnector.java | 7 ++- .../impl/OpenaireFundingDataProvider.java | 15 ++++- .../external/OpenAIRERestConnectorTest.java | 61 +++++++++++++++++++ .../impl/OpenaireFundingDataProviderTest.java | 19 ++++++ 5 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 dspace-api/src/test/java/org/dspace/external/OpenAIRERestConnectorTest.java diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 46bd9ca80d62..963165f75cef 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -8,4 +8,5 @@ on JMockIt Expectations blocks and similar. See https://github.com/checkstyle/checkstyle/issues/3739 --> + diff --git a/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java b/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java index 87af01401ac0..f430959621fc 100644 --- a/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java @@ -206,8 +206,11 @@ public InputStream get(String file, String accessToken) { break; } - // do not close this httpClient - result = getResponse.getEntity().getContent(); + // the client will be closed, we need to copy the response stream to a new one that we can return + try (InputStream is = getResponse.getEntity().getContent()) { + byte[] bytes = is.readAllBytes(); + result = new java.io.ByteArrayInputStream(bytes); + } } } catch (MalformedURLException e1) { getGotError(e1, url + '/' + file); diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java index 62cef508c556..e4243bb065f6 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java @@ -29,6 +29,7 @@ import eu.openaire.oaf.model.base.FundingType; import eu.openaire.oaf.model.base.Project; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Strings; import org.apache.logging.log4j.Logger; import org.dspace.content.dto.MetadataValueDTO; import org.dspace.external.OpenaireRestConnector; @@ -169,7 +170,19 @@ public int getNumberOfResults(String query) { String encodedQuery = encodeValue(query); Response projectResponse = connector.searchProjectByKeywords(0, 0, encodedQuery); - return Integer.parseInt(projectResponse.getHeader().getTotal()); + if (projectResponse == null || projectResponse.getHeader() == null) { + return 0; + } + String total = projectResponse.getHeader().getTotal(); + if (StringUtils.isBlank(total)) { + return 0; + } + try { + return Integer.parseInt(total); + } catch (NumberFormatException e) { + log.error("Failed to parse search result count from OpenAIRE: {}", e.getMessage()); + return 0; + } } /** diff --git a/dspace-api/src/test/java/org/dspace/external/OpenAIRERestConnectorTest.java b/dspace-api/src/test/java/org/dspace/external/OpenAIRERestConnectorTest.java new file mode 100644 index 000000000000..2cb409c4b7fd --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/external/OpenAIRERestConnectorTest.java @@ -0,0 +1,61 @@ +/** + * 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.external; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import eu.openaire.jaxb.model.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.dspace.app.client.DSpaceHttpClientFactory; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + + +public class OpenAIRERestConnectorTest { + + @Test + public void searchProjectByKeywords() throws IOException { + try (InputStream is = this.getClass().getResourceAsStream("openaire-projects.xml"); + MockWebServer mockServer = new MockWebServer()) { + String projects = new String(is.readAllBytes(), StandardCharsets.UTF_8) + .replaceAll("( mushroom)", "( DEADBEEF)"); + mockServer.enqueue(new MockResponse().setResponseCode(200).setBody(projects)); + + // setup mocks so we don't have to set whole DSpace kernel etc. + // still, the idea is to test how the get method behaves + CloseableHttpClient httpClient = spy(HttpClientBuilder.create().build()); + doReturn(httpClient.execute(new HttpGet(mockServer.url("").toString()))) + .when(httpClient).execute(Mockito.any()); + + DSpaceHttpClientFactory mock = Mockito.mock(DSpaceHttpClientFactory.class); + when(mock.build()).thenReturn(httpClient); + + try (MockedStatic mockedFactory = + Mockito.mockStatic(DSpaceHttpClientFactory.class)) { + mockedFactory.when(DSpaceHttpClientFactory::getInstance).thenReturn(mock); + OpenaireRestConnector connector = new OpenaireRestConnector(mockServer.url("").toString()); + Response response = connector.searchProjectByKeywords(0, 10, "keyword"); + // Basically check it doesn't throw UnmarshallerException and that we are getting our mocked response + assertTrue("Expected the query to contain the replaced keyword", + response.getHeader().getQuery().contains("DEADBEEF")); + } + } + } +} diff --git a/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java b/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java index d14dc990353d..fd417f3153e2 100644 --- a/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java @@ -14,7 +14,9 @@ import java.util.List; import java.util.Optional; +import eu.openaire.jaxb.model.Response; import org.dspace.AbstractDSpaceTest; +import org.dspace.external.OpenaireRestConnector; import org.dspace.external.factory.ExternalServiceFactory; import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.ExternalDataProvider; @@ -102,4 +104,21 @@ public void testGetDataObjectWInvalidId() { assertTrue("openaireFunding.getExternalDataObject.notExists:WRONGID", result.isEmpty()); } + + @Test + public void testGetNumberOfResultsWhenResponseIsNull() { + // Create a mock connector that returns null + OpenaireFundingDataProvider provider = new OpenaireFundingDataProvider(); + provider.setSourceIdentifier("test"); + provider.setConnector(new OpenaireRestConnector("test") { + @Override + public Response searchProjectByKeywords(int page, int size, String... keywords) { + return null; + } + }); + + // Should return 0 when response is null, not throw NullPointerException + int result = provider.getNumberOfResults("test"); + assertEquals("Should return 0 when response is null", 0, result); + } } From 5be595d1067c9a27364ae607d97251a424b04d11 Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Wed, 22 Apr 2026 13:31:16 +0200 Subject: [PATCH 608/701] Removed unused import --- .../external/provider/impl/OpenaireFundingDataProvider.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java index e4243bb065f6..ae0f6c89a107 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java @@ -29,7 +29,6 @@ import eu.openaire.oaf.model.base.FundingType; import eu.openaire.oaf.model.base.Project; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Strings; import org.apache.logging.log4j.Logger; import org.dspace.content.dto.MetadataValueDTO; import org.dspace.external.OpenaireRestConnector; From 68c7969bc4eb074995d5cab4695d9213aab02138 Mon Sep 17 00:00:00 2001 From: Tina Schoenborn Date: Tue, 24 Feb 2026 17:34:22 +0100 Subject: [PATCH 609/701] add custom sql count query for signposting endpoint (cherry picked from commit e655bcf94746419288d97b50eebea1bdc6de66db) --- .../java/org/dspace/content/BundleServiceImpl.java | 4 ++++ .../main/java/org/dspace/content/dao/BundleDAO.java | 2 ++ .../org/dspace/content/dao/impl/BundleDAOImpl.java | 10 ++++++++++ .../java/org/dspace/content/service/BundleService.java | 8 ++++++++ .../signposting/service/impl/LinksetServiceImpl.java | 10 +++++++--- 5 files changed, 31 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/BundleServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BundleServiceImpl.java index 3ba90c8cc2ae..8234846ed4f3 100644 --- a/dspace-api/src/main/java/org/dspace/content/BundleServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BundleServiceImpl.java @@ -583,4 +583,8 @@ public Bundle findByLegacyId(Context context, int id) throws SQLException { public int countTotal(Context context) throws SQLException { return bundleDAO.countRows(context); } + + public int countBitstreams(Context context, Bundle bundle) throws SQLException { + return bundleDAO.countBitstreams(context, bundle); + } } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/BundleDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/BundleDAO.java index da7435d46643..99abd84eabc9 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/BundleDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/BundleDAO.java @@ -22,4 +22,6 @@ */ public interface BundleDAO extends DSpaceObjectLegacySupportDAO { int countRows(Context context) throws SQLException; + + int countBitstreams(Context context, Bundle bundle) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/BundleDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/BundleDAOImpl.java index 991636108495..ded13687aa7d 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/BundleDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/BundleDAOImpl.java @@ -9,6 +9,7 @@ import java.sql.SQLException; +import jakarta.persistence.Query; import org.dspace.content.Bundle; import org.dspace.content.dao.BundleDAO; import org.dspace.core.AbstractHibernateDSODAO; @@ -31,4 +32,13 @@ protected BundleDAOImpl() { public int countRows(Context context) throws SQLException { return count(createQuery(context, "SELECT count(*) from Bundle")); } + + @Override + public int countBitstreams(Context context, Bundle bundle) throws SQLException { + Query query = createQuery( + context, "SELECT count(bi.id) from Bundle bu join bu.bitstreams bi where bu.id = :bundleID" + ); + query.setParameter("bundleID", bundle.getID()); + return count(query); + } } diff --git a/dspace-api/src/main/java/org/dspace/content/service/BundleService.java b/dspace-api/src/main/java/org/dspace/content/service/BundleService.java index 10d6613b2a22..31e0d79f4907 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/BundleService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/BundleService.java @@ -146,4 +146,12 @@ public void moveBitstreamToBundle(Context context, Bundle targetBundle, Bitstre public void setOrder(Context context, Bundle bundle, UUID bitstreamIds[]) throws AuthorizeException, SQLException; int countTotal(Context context) throws SQLException; + + /** + * Returns the count of bitstreams for the given bundle, performance optimized. + * + * @param context DSpace Context + * @param bundle the bitstream bundle + */ + int countBitstreams(Context context, Bundle bundle) throws SQLException; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java index b439a46eda6f..cd5d1a62b798 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/signposting/service/impl/LinksetServiceImpl.java @@ -26,6 +26,7 @@ import org.dspace.content.Bundle; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; +import org.dspace.content.service.BundleService; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -48,6 +49,9 @@ public class LinksetServiceImpl implements LinksetService { @Autowired protected ItemService itemService; + @Autowired + protected BundleService bundleService; + @Autowired private BitstreamMetadataReadPermissionEvaluatorPlugin bitstreamMetadataReadPermissionEvaluatorPlugin; @@ -86,7 +90,7 @@ public List createLinksetNodesForSingleLinkset( List linksetNodes = new ArrayList<>(); if (object.getType() == Constants.ITEM) { - int itemBitstreamsCount = countItemBitstreams((Item) object); + int itemBitstreamsCount = countItemBitstreams(context, (Item) object); // Do not include individual bitstream typed links if their number exceeds // the limit in the configuration. @@ -170,12 +174,12 @@ private Iterator getItemBitstreams(Context context, Item item) { } } - private int countItemBitstreams(Item item) { + private int countItemBitstreams(Context context, Item item) { try { int countBitstreams = 0; List bundles = itemService.getBundles(item, Constants.DEFAULT_BUNDLE_NAME); for (Bundle bundle: bundles) { - countBitstreams += bundle.getBitstreams().size(); + countBitstreams += bundleService.countBitstreams(context, bundle); } return countBitstreams; } catch (SQLException e) { From f6d7504108bf9fa0de1826ae12dcf94734c0e6e7 Mon Sep 17 00:00:00 2001 From: Francesco Pio Scognamiglio Date: Wed, 15 May 2024 10:39:44 +0200 Subject: [PATCH 610/701] [DURACOM-437] add the mime.types file due to a known issue on the external library iiif-apis (cherry picked from commit 99340f8a3f5d68605b026483811facd2c10f2ebf) (cherry picked from commit f30117ea76a2a12a34b5ad8fc02852dd5d04f225) --- .../additions/src/main/resources/mime.types | 1863 +++++++++++++++++ 1 file changed, 1863 insertions(+) create mode 100644 dspace/modules/additions/src/main/resources/mime.types diff --git a/dspace/modules/additions/src/main/resources/mime.types b/dspace/modules/additions/src/main/resources/mime.types new file mode 100644 index 000000000000..6707e7e794f4 --- /dev/null +++ b/dspace/modules/additions/src/main/resources/mime.types @@ -0,0 +1,1863 @@ +# The mime.types file comes from the iiif-apis library https://github.com/dbmdz/iiif-apis/blob/main/src/main/resources/mime.types. +# The mime.types file is protected by the MIT license defined here https://github.com/dbmdz/iiif-apis/blob/main/LICENSE. + +# Due to this issue https://github.com/dbmdz/iiif-apis/issues/270 +# The mime.types file has been added to the DSpace sources. +# The mime.types file can be removed as soon as the ticket is closed. + + +# This file maps Internet media types to unique file extension(s). +# Although created for httpd, this file is used by many software systems +# and has been placed in the public domain for unlimited redisribution. +# +# The table below contains both registered and (common) unregistered types. +# A type that has no unique extension can be ignored -- they are listed +# here to guide configurations toward known types and to make it easier to +# identify "new" types. File extensions are also commonly used to indicate +# content languages and encodings, so choose them carefully. +# +# Internet media types should be registered as described in RFC 4288. +# The registry is at . +# +# MIME type (lowercased) Extensions +# ============================================ ========== +# application/1d-interleaved-parityfec +# application/3gpdash-qoe-report+xml +# application/3gpp-ims+xml +# application/a2l +# application/activemessage +# application/alto-costmap+json +# application/alto-costmapfilter+json +# application/alto-directory+json +# application/alto-endpointcost+json +# application/alto-endpointcostparams+json +# application/alto-endpointprop+json +# application/alto-endpointpropparams+json +# application/alto-error+json +# application/alto-networkmap+json +# application/alto-networkmapfilter+json +# application/aml +application/andrew-inset ez +# application/applefile +application/applixware aw +# application/atf +# application/atfx +application/atom+xml atom +application/atomcat+xml atomcat +# application/atomdeleted+xml +# application/atomicmail +application/atomsvc+xml atomsvc +# application/atxml +# application/auth-policy+xml +# application/bacnet-xdd+zip +# application/batch-smtp +# application/beep+xml +# application/calendar+json +# application/calendar+xml +# application/call-completion +# application/cals-1840 +# application/cbor +# application/ccmp+xml +application/ccxml+xml ccxml +# application/cdfx+xml +application/cdmi-capability cdmia +application/cdmi-container cdmic +application/cdmi-domain cdmid +application/cdmi-object cdmio +application/cdmi-queue cdmiq +# application/cdni +# application/cea +# application/cea-2018+xml +# application/cellml+xml +# application/cfw +# application/cms +# application/cnrp+xml +# application/coap-group+json +# application/commonground +# application/conference-info+xml +# application/cpl+xml +# application/csrattrs +# application/csta+xml +# application/cstadata+xml +# application/csvm+json +application/cu-seeme cu +# application/cybercash +# application/dash+xml +# application/dashdelta +application/davmount+xml davmount +# application/dca-rft +# application/dcd +# application/dec-dx +# application/dialog-info+xml +# application/dicom +# application/dii +# application/dit +# application/dns +application/docbook+xml dbk +# application/dskpp+xml +application/dssc+der dssc +application/dssc+xml xdssc +# application/dvcs +application/ecmascript ecma +# application/edi-consent +# application/edi-x12 +# application/edifact +# application/efi +# application/emergencycalldata.comment+xml +# application/emergencycalldata.deviceinfo+xml +# application/emergencycalldata.providerinfo+xml +# application/emergencycalldata.serviceinfo+xml +# application/emergencycalldata.subscriberinfo+xml +application/emma+xml emma +# application/emotionml+xml +# application/encaprtp +# application/epp+xml +application/epub+zip epub +# application/eshop +# application/example +application/exi exi +# application/fastinfoset +# application/fastsoap +# application/fdt+xml +# application/fits +application/font-tdpfr pfr +# application/framework-attributes+xml +# application/geo+json +application/gml+xml gml +application/gpx+xml gpx +application/gxf gxf +# application/gzip +# application/h224 +# application/held+xml +# application/http +application/hyperstudio stk +# application/ibe-key-request+xml +# application/ibe-pkg-reply+xml +# application/ibe-pp-data +# application/iges +# application/im-iscomposing+xml +# application/index +# application/index.cmd +# application/index.obj +# application/index.response +# application/index.vnd +application/inkml+xml ink inkml +# application/iotp +application/ipfix ipfix +# application/ipp +# application/isup +# application/its+xml +application/java-archive jar +application/java-serialized-object ser +application/java-vm class +application/javascript js +# application/jose +# application/jose+json +# application/jrd+json +application/json json +# application/json-patch+json +# application/json-seq +application/jsonml+json jsonml +# application/jwk+json +# application/jwk-set+json +# application/jwt +# application/kpml-request+xml +# application/kpml-response+xml +# application/ld+json +# application/lgr+xml +# application/link-format +# application/load-control+xml +application/lost+xml lostxml +# application/lostsync+xml +# application/lxf +application/mac-binhex40 hqx +application/mac-compactpro cpt +# application/macwriteii +application/mads+xml mads +application/marc mrc +application/marcxml+xml mrcx +application/mathematica ma nb mb +application/mathml+xml mathml +# application/mathml-content+xml +# application/mathml-presentation+xml +# application/mbms-associated-procedure-description+xml +# application/mbms-deregister+xml +# application/mbms-envelope+xml +# application/mbms-msk+xml +# application/mbms-msk-response+xml +# application/mbms-protection-description+xml +# application/mbms-reception-report+xml +# application/mbms-register+xml +# application/mbms-register-response+xml +# application/mbms-schedule+xml +# application/mbms-user-service-description+xml +application/mbox mbox +# application/media-policy-dataset+xml +# application/media_control+xml +application/mediaservercontrol+xml mscml +# application/merge-patch+json +application/metalink+xml metalink +application/metalink4+xml meta4 +application/mets+xml mets +# application/mf4 +# application/mikey +application/mods+xml mods +# application/moss-keys +# application/moss-signature +# application/mosskey-data +# application/mosskey-request +application/mp21 m21 mp21 +application/mp4 mp4s +# application/mpeg4-generic +# application/mpeg4-iod +# application/mpeg4-iod-xmt +# application/mrb-consumer+xml +# application/mrb-publish+xml +# application/msc-ivr+xml +# application/msc-mixer+xml +application/msword doc dot +application/mxf mxf +# application/nasdata +# application/news-checkgroups +# application/news-groupinfo +# application/news-transmission +# application/nlsml+xml +# application/nss +# application/ocsp-request +# application/ocsp-response +application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy +application/oda oda +# application/odx +application/oebps-package+xml opf +application/ogg ogx +application/omdoc+xml omdoc +application/onenote onetoc onetoc2 onetmp onepkg +application/oxps oxps +# application/p2p-overlay+xml +# application/parityfec +application/patch-ops-error+xml xer +application/pdf pdf +# application/pdx +application/pgp-encrypted pgp +# application/pgp-keys +application/pgp-signature asc sig +application/pics-rules prf +# application/pidf+xml +# application/pidf-diff+xml +application/pkcs10 p10 +# application/pkcs12 +application/pkcs7-mime p7m p7c +application/pkcs7-signature p7s +application/pkcs8 p8 +application/pkix-attr-cert ac +application/pkix-cert cer +application/pkix-crl crl +application/pkix-pkipath pkipath +application/pkixcmp pki +application/pls+xml pls +# application/poc-settings+xml +application/postscript ai eps ps +# application/ppsp-tracker+json +# application/problem+json +# application/problem+xml +# application/provenance+xml +# application/prs.alvestrand.titrax-sheet +application/prs.cww cww +# application/prs.hpub+zip +# application/prs.nprend +# application/prs.plucker +# application/prs.rdf-xml-crypt +# application/prs.xsf+xml +application/pskc+xml pskcxml +# application/qsig +# application/raptorfec +# application/rdap+json +application/rdf+xml rdf +application/reginfo+xml rif +application/relax-ng-compact-syntax rnc +# application/remote-printing +# application/reputon+json +application/resource-lists+xml rl +application/resource-lists-diff+xml rld +# application/rfc+xml +# application/riscos +# application/rlmi+xml +application/rls-services+xml rs +application/rpki-ghostbusters gbr +application/rpki-manifest mft +application/rpki-roa roa +# application/rpki-updown +application/rsd+xml rsd +application/rss+xml rss +application/rtf rtf +# application/rtploopback +# application/rtx +# application/samlassertion+xml +# application/samlmetadata+xml +application/sbml+xml sbml +# application/scaip+xml +# application/scim+json +application/scvp-cv-request scq +application/scvp-cv-response scs +application/scvp-vp-request spq +application/scvp-vp-response spp +application/sdp sdp +# application/sep+xml +# application/sep-exi +# application/session-info +# application/set-payment +application/set-payment-initiation setpay +# application/set-registration +application/set-registration-initiation setreg +# application/sgml +# application/sgml-open-catalog +application/shf+xml shf +# application/sieve +# application/simple-filter+xml +# application/simple-message-summary +# application/simplesymbolcontainer +# application/slate +# application/smil +application/smil+xml smi smil +# application/smpte336m +# application/soap+fastinfoset +# application/soap+xml +application/sparql-query rq +application/sparql-results+xml srx +# application/spirits-event+xml +# application/sql +application/srgs gram +application/srgs+xml grxml +application/sru+xml sru +application/ssdl+xml ssdl +application/ssml+xml ssml +# application/tamp-apex-update +# application/tamp-apex-update-confirm +# application/tamp-community-update +# application/tamp-community-update-confirm +# application/tamp-error +# application/tamp-sequence-adjust +# application/tamp-sequence-adjust-confirm +# application/tamp-status-query +# application/tamp-status-response +# application/tamp-update +# application/tamp-update-confirm +application/tei+xml tei teicorpus +application/thraud+xml tfi +# application/timestamp-query +# application/timestamp-reply +application/timestamped-data tsd +# application/ttml+xml +# application/tve-trigger +# application/ulpfec +# application/urc-grpsheet+xml +# application/urc-ressheet+xml +# application/urc-targetdesc+xml +# application/urc-uisocketdesc+xml +# application/vcard+json +# application/vcard+xml +# application/vemmi +# application/vividence.scriptfile +# application/vnd.3gpp-prose+xml +# application/vnd.3gpp-prose-pc3ch+xml +# application/vnd.3gpp.access-transfer-events+xml +# application/vnd.3gpp.bsf+xml +# application/vnd.3gpp.mid-call+xml +application/vnd.3gpp.pic-bw-large plb +application/vnd.3gpp.pic-bw-small psb +application/vnd.3gpp.pic-bw-var pvb +# application/vnd.3gpp.sms +# application/vnd.3gpp.sms+xml +# application/vnd.3gpp.srvcc-ext+xml +# application/vnd.3gpp.srvcc-info+xml +# application/vnd.3gpp.state-and-event-info+xml +# application/vnd.3gpp.ussd+xml +# application/vnd.3gpp2.bcmcsinfo+xml +# application/vnd.3gpp2.sms +application/vnd.3gpp2.tcap tcap +# application/vnd.3lightssoftware.imagescal +application/vnd.3m.post-it-notes pwn +application/vnd.accpac.simply.aso aso +application/vnd.accpac.simply.imp imp +application/vnd.acucobol acu +application/vnd.acucorp atc acutc +application/vnd.adobe.air-application-installer-package+zip air +# application/vnd.adobe.flash.movie +application/vnd.adobe.formscentral.fcdt fcdt +application/vnd.adobe.fxp fxp fxpl +# application/vnd.adobe.partial-upload +application/vnd.adobe.xdp+xml xdp +application/vnd.adobe.xfdf xfdf +# application/vnd.aether.imp +# application/vnd.ah-barcode +application/vnd.ahead.space ahead +application/vnd.airzip.filesecure.azf azf +application/vnd.airzip.filesecure.azs azs +application/vnd.amazon.ebook azw +# application/vnd.amazon.mobi8-ebook +application/vnd.americandynamics.acc acc +application/vnd.amiga.ami ami +# application/vnd.amundsen.maze+xml +application/vnd.android.package-archive apk +# application/vnd.anki +application/vnd.anser-web-certificate-issue-initiation cii +application/vnd.anser-web-funds-transfer-initiation fti +application/vnd.antix.game-component atx +# application/vnd.apache.thrift.binary +# application/vnd.apache.thrift.compact +# application/vnd.apache.thrift.json +# application/vnd.api+json +application/vnd.apple.installer+xml mpkg +application/vnd.apple.mpegurl m3u8 +# application/vnd.arastra.swi +application/vnd.aristanetworks.swi swi +# application/vnd.artsquare +application/vnd.astraea-software.iota iota +application/vnd.audiograph aep +# application/vnd.autopackage +# application/vnd.avistar+xml +# application/vnd.balsamiq.bmml+xml +# application/vnd.balsamiq.bmpr +# application/vnd.bekitzur-stech+json +# application/vnd.biopax.rdf+xml +application/vnd.blueice.multipass mpm +# application/vnd.bluetooth.ep.oob +# application/vnd.bluetooth.le.oob +application/vnd.bmi bmi +application/vnd.businessobjects rep +# application/vnd.cab-jscript +# application/vnd.canon-cpdl +# application/vnd.canon-lips +# application/vnd.cendio.thinlinc.clientconf +# application/vnd.century-systems.tcp_stream +application/vnd.chemdraw+xml cdxml +# application/vnd.chess-pgn +application/vnd.chipnuts.karaoke-mmd mmd +application/vnd.cinderella cdy +# application/vnd.cirpack.isdn-ext +# application/vnd.citationstyles.style+xml +application/vnd.claymore cla +application/vnd.cloanto.rp9 rp9 +application/vnd.clonk.c4group c4g c4d c4f c4p c4u +application/vnd.cluetrust.cartomobile-config c11amc +application/vnd.cluetrust.cartomobile-config-pkg c11amz +# application/vnd.coffeescript +# application/vnd.collection+json +# application/vnd.collection.doc+json +# application/vnd.collection.next+json +# application/vnd.comicbook+zip +# application/vnd.commerce-battelle +application/vnd.commonspace csp +application/vnd.contact.cmsg cdbcmsg +# application/vnd.coreos.ignition+json +application/vnd.cosmocaller cmc +application/vnd.crick.clicker clkx +application/vnd.crick.clicker.keyboard clkk +application/vnd.crick.clicker.palette clkp +application/vnd.crick.clicker.template clkt +application/vnd.crick.clicker.wordbank clkw +application/vnd.criticaltools.wbs+xml wbs +application/vnd.ctc-posml pml +# application/vnd.ctct.ws+xml +# application/vnd.cups-pdf +# application/vnd.cups-postscript +application/vnd.cups-ppd ppd +# application/vnd.cups-raster +# application/vnd.cups-raw +# application/vnd.curl +application/vnd.curl.car car +application/vnd.curl.pcurl pcurl +# application/vnd.cyan.dean.root+xml +# application/vnd.cybank +application/vnd.dart dart +application/vnd.data-vision.rdz rdz +# application/vnd.debian.binary-package +application/vnd.dece.data uvf uvvf uvd uvvd +application/vnd.dece.ttml+xml uvt uvvt +application/vnd.dece.unspecified uvx uvvx +application/vnd.dece.zip uvz uvvz +application/vnd.denovo.fcselayout-link fe_launch +# application/vnd.desmume.movie +# application/vnd.dir-bi.plate-dl-nosuffix +# application/vnd.dm.delegation+xml +application/vnd.dna dna +# application/vnd.document+json +application/vnd.dolby.mlp mlp +# application/vnd.dolby.mobile.1 +# application/vnd.dolby.mobile.2 +# application/vnd.doremir.scorecloud-binary-document +application/vnd.dpgraph dpg +application/vnd.dreamfactory dfac +# application/vnd.drive+json +application/vnd.ds-keypoint kpxx +# application/vnd.dtg.local +# application/vnd.dtg.local.flash +# application/vnd.dtg.local.html +application/vnd.dvb.ait ait +# application/vnd.dvb.dvbj +# application/vnd.dvb.esgcontainer +# application/vnd.dvb.ipdcdftnotifaccess +# application/vnd.dvb.ipdcesgaccess +# application/vnd.dvb.ipdcesgaccess2 +# application/vnd.dvb.ipdcesgpdd +# application/vnd.dvb.ipdcroaming +# application/vnd.dvb.iptv.alfec-base +# application/vnd.dvb.iptv.alfec-enhancement +# application/vnd.dvb.notif-aggregate-root+xml +# application/vnd.dvb.notif-container+xml +# application/vnd.dvb.notif-generic+xml +# application/vnd.dvb.notif-ia-msglist+xml +# application/vnd.dvb.notif-ia-registration-request+xml +# application/vnd.dvb.notif-ia-registration-response+xml +# application/vnd.dvb.notif-init+xml +# application/vnd.dvb.pfr +application/vnd.dvb.service svc +# application/vnd.dxr +application/vnd.dynageo geo +# application/vnd.dzr +# application/vnd.easykaraoke.cdgdownload +# application/vnd.ecdis-update +application/vnd.ecowin.chart mag +# application/vnd.ecowin.filerequest +# application/vnd.ecowin.fileupdate +# application/vnd.ecowin.series +# application/vnd.ecowin.seriesrequest +# application/vnd.ecowin.seriesupdate +# application/vnd.emclient.accessrequest+xml +application/vnd.enliven nml +# application/vnd.enphase.envoy +# application/vnd.eprints.data+xml +application/vnd.epson.esf esf +application/vnd.epson.msf msf +application/vnd.epson.quickanime qam +application/vnd.epson.salt slt +application/vnd.epson.ssf ssf +# application/vnd.ericsson.quickcall +application/vnd.eszigno3+xml es3 et3 +# application/vnd.etsi.aoc+xml +# application/vnd.etsi.asic-e+zip +# application/vnd.etsi.asic-s+zip +# application/vnd.etsi.cug+xml +# application/vnd.etsi.iptvcommand+xml +# application/vnd.etsi.iptvdiscovery+xml +# application/vnd.etsi.iptvprofile+xml +# application/vnd.etsi.iptvsad-bc+xml +# application/vnd.etsi.iptvsad-cod+xml +# application/vnd.etsi.iptvsad-npvr+xml +# application/vnd.etsi.iptvservice+xml +# application/vnd.etsi.iptvsync+xml +# application/vnd.etsi.iptvueprofile+xml +# application/vnd.etsi.mcid+xml +# application/vnd.etsi.mheg5 +# application/vnd.etsi.overload-control-policy-dataset+xml +# application/vnd.etsi.pstn+xml +# application/vnd.etsi.sci+xml +# application/vnd.etsi.simservs+xml +# application/vnd.etsi.timestamp-token +# application/vnd.etsi.tsl+xml +# application/vnd.etsi.tsl.der +# application/vnd.eudora.data +application/vnd.ezpix-album ez2 +application/vnd.ezpix-package ez3 +# application/vnd.f-secure.mobile +# application/vnd.fastcopy-disk-image +application/vnd.fdf fdf +application/vnd.fdsn.mseed mseed +application/vnd.fdsn.seed seed dataless +# application/vnd.ffsns +# application/vnd.filmit.zfc +# application/vnd.fints +# application/vnd.firemonkeys.cloudcell +application/vnd.flographit gph +application/vnd.fluxtime.clip ftc +# application/vnd.font-fontforge-sfd +application/vnd.framemaker fm frame maker book +application/vnd.frogans.fnc fnc +application/vnd.frogans.ltf ltf +application/vnd.fsc.weblaunch fsc +application/vnd.fujitsu.oasys oas +application/vnd.fujitsu.oasys2 oa2 +application/vnd.fujitsu.oasys3 oa3 +application/vnd.fujitsu.oasysgp fg5 +application/vnd.fujitsu.oasysprs bh2 +# application/vnd.fujixerox.art-ex +# application/vnd.fujixerox.art4 +application/vnd.fujixerox.ddd ddd +application/vnd.fujixerox.docuworks xdw +application/vnd.fujixerox.docuworks.binder xbd +# application/vnd.fujixerox.docuworks.container +# application/vnd.fujixerox.hbpl +# application/vnd.fut-misnet +application/vnd.fuzzysheet fzs +application/vnd.genomatix.tuxedo txd +# application/vnd.geo+json +# application/vnd.geocube+xml +application/vnd.geogebra.file ggb +application/vnd.geogebra.tool ggt +application/vnd.geometry-explorer gex gre +application/vnd.geonext gxt +application/vnd.geoplan g2w +application/vnd.geospace g3w +# application/vnd.gerber +# application/vnd.globalplatform.card-content-mgt +# application/vnd.globalplatform.card-content-mgt-response +application/vnd.gmx gmx +application/vnd.google-earth.kml+xml kml +application/vnd.google-earth.kmz kmz +# application/vnd.gov.sk.e-form+xml +# application/vnd.gov.sk.e-form+zip +# application/vnd.gov.sk.xmldatacontainer+xml +application/vnd.grafeq gqf gqs +# application/vnd.gridmp +application/vnd.groove-account gac +application/vnd.groove-help ghf +application/vnd.groove-identity-message gim +application/vnd.groove-injector grv +application/vnd.groove-tool-message gtm +application/vnd.groove-tool-template tpl +application/vnd.groove-vcard vcg +# application/vnd.hal+json +application/vnd.hal+xml hal +application/vnd.handheld-entertainment+xml zmm +application/vnd.hbci hbci +# application/vnd.hcl-bireports +# application/vnd.hdt +# application/vnd.heroku+json +application/vnd.hhe.lesson-player les +application/vnd.hp-hpgl hpgl +application/vnd.hp-hpid hpid +application/vnd.hp-hps hps +application/vnd.hp-jlyt jlt +application/vnd.hp-pcl pcl +application/vnd.hp-pclxl pclxl +# application/vnd.httphone +application/vnd.hydrostatix.sof-data sfd-hdstx +# application/vnd.hyperdrive+json +# application/vnd.hzn-3d-crossword +# application/vnd.ibm.afplinedata +# application/vnd.ibm.electronic-media +application/vnd.ibm.minipay mpy +application/vnd.ibm.modcap afp listafp list3820 +application/vnd.ibm.rights-management irm +application/vnd.ibm.secure-container sc +application/vnd.iccprofile icc icm +# application/vnd.ieee.1905 +application/vnd.igloader igl +application/vnd.immervision-ivp ivp +application/vnd.immervision-ivu ivu +# application/vnd.ims.imsccv1p1 +# application/vnd.ims.imsccv1p2 +# application/vnd.ims.imsccv1p3 +# application/vnd.ims.lis.v2.result+json +# application/vnd.ims.lti.v2.toolconsumerprofile+json +# application/vnd.ims.lti.v2.toolproxy+json +# application/vnd.ims.lti.v2.toolproxy.id+json +# application/vnd.ims.lti.v2.toolsettings+json +# application/vnd.ims.lti.v2.toolsettings.simple+json +# application/vnd.informedcontrol.rms+xml +# application/vnd.informix-visionary +# application/vnd.infotech.project +# application/vnd.infotech.project+xml +# application/vnd.innopath.wamp.notification +application/vnd.insors.igm igm +application/vnd.intercon.formnet xpw xpx +application/vnd.intergeo i2g +# application/vnd.intertrust.digibox +# application/vnd.intertrust.nncp +application/vnd.intu.qbo qbo +application/vnd.intu.qfx qfx +# application/vnd.iptc.g2.catalogitem+xml +# application/vnd.iptc.g2.conceptitem+xml +# application/vnd.iptc.g2.knowledgeitem+xml +# application/vnd.iptc.g2.newsitem+xml +# application/vnd.iptc.g2.newsmessage+xml +# application/vnd.iptc.g2.packageitem+xml +# application/vnd.iptc.g2.planningitem+xml +application/vnd.ipunplugged.rcprofile rcprofile +application/vnd.irepository.package+xml irp +application/vnd.is-xpr xpr +application/vnd.isac.fcs fcs +application/vnd.jam jam +# application/vnd.japannet-directory-service +# application/vnd.japannet-jpnstore-wakeup +# application/vnd.japannet-payment-wakeup +# application/vnd.japannet-registration +# application/vnd.japannet-registration-wakeup +# application/vnd.japannet-setstore-wakeup +# application/vnd.japannet-verification +# application/vnd.japannet-verification-wakeup +application/vnd.jcp.javame.midlet-rms rms +application/vnd.jisp jisp +application/vnd.joost.joda-archive joda +# application/vnd.jsk.isdn-ngn +application/vnd.kahootz ktz ktr +application/vnd.kde.karbon karbon +application/vnd.kde.kchart chrt +application/vnd.kde.kformula kfo +application/vnd.kde.kivio flw +application/vnd.kde.kontour kon +application/vnd.kde.kpresenter kpr kpt +application/vnd.kde.kspread ksp +application/vnd.kde.kword kwd kwt +application/vnd.kenameaapp htke +application/vnd.kidspiration kia +application/vnd.kinar kne knp +application/vnd.koan skp skd skt skm +application/vnd.kodak-descriptor sse +application/vnd.las.las+xml lasxml +# application/vnd.liberty-request+xml +application/vnd.llamagraphics.life-balance.desktop lbd +application/vnd.llamagraphics.life-balance.exchange+xml lbe +application/vnd.lotus-1-2-3 123 +application/vnd.lotus-approach apr +application/vnd.lotus-freelance pre +application/vnd.lotus-notes nsf +application/vnd.lotus-organizer org +application/vnd.lotus-screencam scm +application/vnd.lotus-wordpro lwp +application/vnd.macports.portpkg portpkg +# application/vnd.mapbox-vector-tile +# application/vnd.marlin.drm.actiontoken+xml +# application/vnd.marlin.drm.conftoken+xml +# application/vnd.marlin.drm.license+xml +# application/vnd.marlin.drm.mdcf +# application/vnd.mason+json +# application/vnd.maxmind.maxmind-db +application/vnd.mcd mcd +application/vnd.medcalcdata mc1 +application/vnd.mediastation.cdkey cdkey +# application/vnd.meridian-slingshot +application/vnd.mfer mwf +application/vnd.mfmp mfm +# application/vnd.micro+json +application/vnd.micrografx.flo flo +application/vnd.micrografx.igx igx +# application/vnd.microsoft.portable-executable +# application/vnd.miele+json +application/vnd.mif mif +# application/vnd.minisoft-hp3000-save +# application/vnd.mitsubishi.misty-guard.trustweb +application/vnd.mobius.daf daf +application/vnd.mobius.dis dis +application/vnd.mobius.mbk mbk +application/vnd.mobius.mqy mqy +application/vnd.mobius.msl msl +application/vnd.mobius.plc plc +application/vnd.mobius.txf txf +application/vnd.mophun.application mpn +application/vnd.mophun.certificate mpc +# application/vnd.motorola.flexsuite +# application/vnd.motorola.flexsuite.adsi +# application/vnd.motorola.flexsuite.fis +# application/vnd.motorola.flexsuite.gotap +# application/vnd.motorola.flexsuite.kmr +# application/vnd.motorola.flexsuite.ttc +# application/vnd.motorola.flexsuite.wem +# application/vnd.motorola.iprm +application/vnd.mozilla.xul+xml xul +# application/vnd.ms-3mfdocument +application/vnd.ms-artgalry cil +# application/vnd.ms-asf +application/vnd.ms-cab-compressed cab +# application/vnd.ms-color.iccprofile +application/vnd.ms-excel xls xlm xla xlc xlt xlw +application/vnd.ms-excel.addin.macroenabled.12 xlam +application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb +application/vnd.ms-excel.sheet.macroenabled.12 xlsm +application/vnd.ms-excel.template.macroenabled.12 xltm +application/vnd.ms-fontobject eot +application/vnd.ms-htmlhelp chm +application/vnd.ms-ims ims +application/vnd.ms-lrm lrm +# application/vnd.ms-office.activex+xml +application/vnd.ms-officetheme thmx +# application/vnd.ms-opentype +# application/vnd.ms-package.obfuscated-opentype +application/vnd.ms-pki.seccat cat +application/vnd.ms-pki.stl stl +# application/vnd.ms-playready.initiator+xml +application/vnd.ms-powerpoint ppt pps pot +application/vnd.ms-powerpoint.addin.macroenabled.12 ppam +application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm +application/vnd.ms-powerpoint.slide.macroenabled.12 sldm +application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm +application/vnd.ms-powerpoint.template.macroenabled.12 potm +# application/vnd.ms-printdevicecapabilities+xml +# application/vnd.ms-printing.printticket+xml +# application/vnd.ms-printschematicket+xml +application/vnd.ms-project mpp mpt +# application/vnd.ms-tnef +# application/vnd.ms-windows.devicepairing +# application/vnd.ms-windows.nwprinting.oob +# application/vnd.ms-windows.printerpairing +# application/vnd.ms-windows.wsd.oob +# application/vnd.ms-wmdrm.lic-chlg-req +# application/vnd.ms-wmdrm.lic-resp +# application/vnd.ms-wmdrm.meter-chlg-req +# application/vnd.ms-wmdrm.meter-resp +application/vnd.ms-word.document.macroenabled.12 docm +application/vnd.ms-word.template.macroenabled.12 dotm +application/vnd.ms-works wps wks wcm wdb +application/vnd.ms-wpl wpl +application/vnd.ms-xpsdocument xps +# application/vnd.msa-disk-image +application/vnd.mseq mseq +# application/vnd.msign +# application/vnd.multiad.creator +# application/vnd.multiad.creator.cif +# application/vnd.music-niff +application/vnd.musician mus +application/vnd.muvee.style msty +application/vnd.mynfc taglet +# application/vnd.ncd.control +# application/vnd.ncd.reference +# application/vnd.nervana +# application/vnd.netfpx +application/vnd.neurolanguage.nlu nlu +# application/vnd.nintendo.nitro.rom +# application/vnd.nintendo.snes.rom +application/vnd.nitf ntf nitf +application/vnd.noblenet-directory nnd +application/vnd.noblenet-sealer nns +application/vnd.noblenet-web nnw +# application/vnd.nokia.catalogs +# application/vnd.nokia.conml+wbxml +# application/vnd.nokia.conml+xml +# application/vnd.nokia.iptv.config+xml +# application/vnd.nokia.isds-radio-presets +# application/vnd.nokia.landmark+wbxml +# application/vnd.nokia.landmark+xml +# application/vnd.nokia.landmarkcollection+xml +# application/vnd.nokia.n-gage.ac+xml +application/vnd.nokia.n-gage.data ngdat +application/vnd.nokia.n-gage.symbian.install n-gage +# application/vnd.nokia.ncd +# application/vnd.nokia.pcd+wbxml +# application/vnd.nokia.pcd+xml +application/vnd.nokia.radio-preset rpst +application/vnd.nokia.radio-presets rpss +application/vnd.novadigm.edm edm +application/vnd.novadigm.edx edx +application/vnd.novadigm.ext ext +# application/vnd.ntt-local.content-share +# application/vnd.ntt-local.file-transfer +# application/vnd.ntt-local.ogw_remote-access +# application/vnd.ntt-local.sip-ta_remote +# application/vnd.ntt-local.sip-ta_tcp_stream +application/vnd.oasis.opendocument.chart odc +application/vnd.oasis.opendocument.chart-template otc +application/vnd.oasis.opendocument.database odb +application/vnd.oasis.opendocument.formula odf +application/vnd.oasis.opendocument.formula-template odft +application/vnd.oasis.opendocument.graphics odg +application/vnd.oasis.opendocument.graphics-template otg +application/vnd.oasis.opendocument.image odi +application/vnd.oasis.opendocument.image-template oti +application/vnd.oasis.opendocument.presentation odp +application/vnd.oasis.opendocument.presentation-template otp +application/vnd.oasis.opendocument.spreadsheet ods +application/vnd.oasis.opendocument.spreadsheet-template ots +application/vnd.oasis.opendocument.text odt +application/vnd.oasis.opendocument.text-master odm +application/vnd.oasis.opendocument.text-template ott +application/vnd.oasis.opendocument.text-web oth +# application/vnd.obn +# application/vnd.oftn.l10n+json +# application/vnd.oipf.contentaccessdownload+xml +# application/vnd.oipf.contentaccessstreaming+xml +# application/vnd.oipf.cspg-hexbinary +# application/vnd.oipf.dae.svg+xml +# application/vnd.oipf.dae.xhtml+xml +# application/vnd.oipf.mippvcontrolmessage+xml +# application/vnd.oipf.pae.gem +# application/vnd.oipf.spdiscovery+xml +# application/vnd.oipf.spdlist+xml +# application/vnd.oipf.ueprofile+xml +# application/vnd.oipf.userprofile+xml +application/vnd.olpc-sugar xo +# application/vnd.oma-scws-config +# application/vnd.oma-scws-http-request +# application/vnd.oma-scws-http-response +# application/vnd.oma.bcast.associated-procedure-parameter+xml +# application/vnd.oma.bcast.drm-trigger+xml +# application/vnd.oma.bcast.imd+xml +# application/vnd.oma.bcast.ltkm +# application/vnd.oma.bcast.notification+xml +# application/vnd.oma.bcast.provisioningtrigger +# application/vnd.oma.bcast.sgboot +# application/vnd.oma.bcast.sgdd+xml +# application/vnd.oma.bcast.sgdu +# application/vnd.oma.bcast.simple-symbol-container +# application/vnd.oma.bcast.smartcard-trigger+xml +# application/vnd.oma.bcast.sprov+xml +# application/vnd.oma.bcast.stkm +# application/vnd.oma.cab-address-book+xml +# application/vnd.oma.cab-feature-handler+xml +# application/vnd.oma.cab-pcc+xml +# application/vnd.oma.cab-subs-invite+xml +# application/vnd.oma.cab-user-prefs+xml +# application/vnd.oma.dcd +# application/vnd.oma.dcdc +application/vnd.oma.dd2+xml dd2 +# application/vnd.oma.drm.risd+xml +# application/vnd.oma.group-usage-list+xml +# application/vnd.oma.lwm2m+json +# application/vnd.oma.lwm2m+tlv +# application/vnd.oma.pal+xml +# application/vnd.oma.poc.detailed-progress-report+xml +# application/vnd.oma.poc.final-report+xml +# application/vnd.oma.poc.groups+xml +# application/vnd.oma.poc.invocation-descriptor+xml +# application/vnd.oma.poc.optimized-progress-report+xml +# application/vnd.oma.push +# application/vnd.oma.scidm.messages+xml +# application/vnd.oma.xcap-directory+xml +# application/vnd.omads-email+xml +# application/vnd.omads-file+xml +# application/vnd.omads-folder+xml +# application/vnd.omaloc-supl-init +# application/vnd.onepager +# application/vnd.openblox.game+xml +# application/vnd.openblox.game-binary +# application/vnd.openeye.oeb +application/vnd.openofficeorg.extension oxt +# application/vnd.openxmlformats-officedocument.custom-properties+xml +# application/vnd.openxmlformats-officedocument.customxmlproperties+xml +# application/vnd.openxmlformats-officedocument.drawing+xml +# application/vnd.openxmlformats-officedocument.drawingml.chart+xml +# application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml +# application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml +# application/vnd.openxmlformats-officedocument.extended-properties+xml +# application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml +# application/vnd.openxmlformats-officedocument.presentationml.comments+xml +# application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml +# application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml +application/vnd.openxmlformats-officedocument.presentationml.presentation pptx +# application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.presprops+xml +application/vnd.openxmlformats-officedocument.presentationml.slide sldx +# application/vnd.openxmlformats-officedocument.presentationml.slide+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml +# application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml +application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx +# application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml +# application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml +# application/vnd.openxmlformats-officedocument.presentationml.tags+xml +application/vnd.openxmlformats-officedocument.presentationml.template potx +# application/vnd.openxmlformats-officedocument.presentationml.template.main+xml +# application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml +application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx +# application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml +# application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml +# application/vnd.openxmlformats-officedocument.theme+xml +# application/vnd.openxmlformats-officedocument.themeoverride+xml +# application/vnd.openxmlformats-officedocument.vmldrawing +# application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.document docx +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml +application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx +# application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml +# application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml +# application/vnd.openxmlformats-package.core-properties+xml +# application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml +# application/vnd.openxmlformats-package.relationships+xml +# application/vnd.oracle.resource+json +# application/vnd.orange.indata +# application/vnd.osa.netdeploy +application/vnd.osgeo.mapguide.package mgp +# application/vnd.osgi.bundle +application/vnd.osgi.dp dp +application/vnd.osgi.subsystem esa +# application/vnd.otps.ct-kip+xml +# application/vnd.oxli.countgraph +# application/vnd.pagerduty+json +application/vnd.palm pdb pqa oprc +# application/vnd.panoply +# application/vnd.paos.xml +application/vnd.pawaafile paw +# application/vnd.pcos +application/vnd.pg.format str +application/vnd.pg.osasli ei6 +# application/vnd.piaccess.application-licence +application/vnd.picsel efif +application/vnd.pmi.widget wg +# application/vnd.poc.group-advertisement+xml +application/vnd.pocketlearn plf +application/vnd.powerbuilder6 pbd +# application/vnd.powerbuilder6-s +# application/vnd.powerbuilder7 +# application/vnd.powerbuilder7-s +# application/vnd.powerbuilder75 +# application/vnd.powerbuilder75-s +# application/vnd.preminet +application/vnd.previewsystems.box box +application/vnd.proteus.magazine mgz +application/vnd.publishare-delta-tree qps +application/vnd.pvi.ptid1 ptid +# application/vnd.pwg-multiplexed +# application/vnd.pwg-xhtml-print+xml +# application/vnd.qualcomm.brew-app-res +# application/vnd.quarantainenet +application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb +# application/vnd.quobject-quoxdocument +# application/vnd.radisys.moml+xml +# application/vnd.radisys.msml+xml +# application/vnd.radisys.msml-audit+xml +# application/vnd.radisys.msml-audit-conf+xml +# application/vnd.radisys.msml-audit-conn+xml +# application/vnd.radisys.msml-audit-dialog+xml +# application/vnd.radisys.msml-audit-stream+xml +# application/vnd.radisys.msml-conf+xml +# application/vnd.radisys.msml-dialog+xml +# application/vnd.radisys.msml-dialog-base+xml +# application/vnd.radisys.msml-dialog-fax-detect+xml +# application/vnd.radisys.msml-dialog-fax-sendrecv+xml +# application/vnd.radisys.msml-dialog-group+xml +# application/vnd.radisys.msml-dialog-speech+xml +# application/vnd.radisys.msml-dialog-transform+xml +# application/vnd.rainstor.data +# application/vnd.rapid +# application/vnd.rar +application/vnd.realvnc.bed bed +application/vnd.recordare.musicxml mxl +application/vnd.recordare.musicxml+xml musicxml +# application/vnd.renlearn.rlprint +application/vnd.rig.cryptonote cryptonote +application/vnd.rim.cod cod +application/vnd.rn-realmedia rm +application/vnd.rn-realmedia-vbr rmvb +application/vnd.route66.link66+xml link66 +# application/vnd.rs-274x +# application/vnd.ruckus.download +# application/vnd.s3sms +application/vnd.sailingtracker.track st +# application/vnd.sbm.cid +# application/vnd.sbm.mid2 +# application/vnd.scribus +# application/vnd.sealed.3df +# application/vnd.sealed.csf +# application/vnd.sealed.doc +# application/vnd.sealed.eml +# application/vnd.sealed.mht +# application/vnd.sealed.net +# application/vnd.sealed.ppt +# application/vnd.sealed.tiff +# application/vnd.sealed.xls +# application/vnd.sealedmedia.softseal.html +# application/vnd.sealedmedia.softseal.pdf +application/vnd.seemail see +application/vnd.sema sema +application/vnd.semd semd +application/vnd.semf semf +application/vnd.shana.informed.formdata ifm +application/vnd.shana.informed.formtemplate itp +application/vnd.shana.informed.interchange iif +application/vnd.shana.informed.package ipk +application/vnd.simtech-mindmapper twd twds +# application/vnd.siren+json +application/vnd.smaf mmf +# application/vnd.smart.notebook +application/vnd.smart.teacher teacher +# application/vnd.software602.filler.form+xml +# application/vnd.software602.filler.form-xml-zip +application/vnd.solent.sdkm+xml sdkm sdkd +application/vnd.spotfire.dxp dxp +application/vnd.spotfire.sfs sfs +# application/vnd.sss-cod +# application/vnd.sss-dtf +# application/vnd.sss-ntf +application/vnd.stardivision.calc sdc +application/vnd.stardivision.draw sda +application/vnd.stardivision.impress sdd +application/vnd.stardivision.math smf +application/vnd.stardivision.writer sdw vor +application/vnd.stardivision.writer-global sgl +application/vnd.stepmania.package smzip +application/vnd.stepmania.stepchart sm +# application/vnd.street-stream +# application/vnd.sun.wadl+xml +application/vnd.sun.xml.calc sxc +application/vnd.sun.xml.calc.template stc +application/vnd.sun.xml.draw sxd +application/vnd.sun.xml.draw.template std +application/vnd.sun.xml.impress sxi +application/vnd.sun.xml.impress.template sti +application/vnd.sun.xml.math sxm +application/vnd.sun.xml.writer sxw +application/vnd.sun.xml.writer.global sxg +application/vnd.sun.xml.writer.template stw +application/vnd.sus-calendar sus susp +application/vnd.svd svd +# application/vnd.swiftview-ics +application/vnd.symbian.install sis sisx +application/vnd.syncml+xml xsm +application/vnd.syncml.dm+wbxml bdm +application/vnd.syncml.dm+xml xdm +# application/vnd.syncml.dm.notification +# application/vnd.syncml.dmddf+wbxml +# application/vnd.syncml.dmddf+xml +# application/vnd.syncml.dmtnds+wbxml +# application/vnd.syncml.dmtnds+xml +# application/vnd.syncml.ds.notification +application/vnd.tao.intent-module-archive tao +application/vnd.tcpdump.pcap pcap cap dmp +# application/vnd.tmd.mediaflex.api+xml +# application/vnd.tml +application/vnd.tmobile-livetv tmo +application/vnd.trid.tpt tpt +application/vnd.triscape.mxs mxs +application/vnd.trueapp tra +# application/vnd.truedoc +# application/vnd.ubisoft.webplayer +application/vnd.ufdl ufd ufdl +application/vnd.uiq.theme utz +application/vnd.umajin umj +application/vnd.unity unityweb +application/vnd.uoml+xml uoml +# application/vnd.uplanet.alert +# application/vnd.uplanet.alert-wbxml +# application/vnd.uplanet.bearer-choice +# application/vnd.uplanet.bearer-choice-wbxml +# application/vnd.uplanet.cacheop +# application/vnd.uplanet.cacheop-wbxml +# application/vnd.uplanet.channel +# application/vnd.uplanet.channel-wbxml +# application/vnd.uplanet.list +# application/vnd.uplanet.list-wbxml +# application/vnd.uplanet.listcmd +# application/vnd.uplanet.listcmd-wbxml +# application/vnd.uplanet.signal +# application/vnd.uri-map +# application/vnd.valve.source.material +application/vnd.vcx vcx +# application/vnd.vd-study +# application/vnd.vectorworks +# application/vnd.vel+json +# application/vnd.verimatrix.vcas +# application/vnd.vidsoft.vidconference +application/vnd.visio vsd vst vss vsw +application/vnd.visionary vis +# application/vnd.vividence.scriptfile +application/vnd.vsf vsf +# application/vnd.wap.sic +# application/vnd.wap.slc +application/vnd.wap.wbxml wbxml +application/vnd.wap.wmlc wmlc +application/vnd.wap.wmlscriptc wmlsc +application/vnd.webturbo wtb +# application/vnd.wfa.p2p +# application/vnd.wfa.wsc +# application/vnd.windows.devicepairing +# application/vnd.wmc +# application/vnd.wmf.bootstrap +# application/vnd.wolfram.mathematica +# application/vnd.wolfram.mathematica.package +application/vnd.wolfram.player nbp +application/vnd.wordperfect wpd +application/vnd.wqd wqd +# application/vnd.wrq-hp3000-labelled +application/vnd.wt.stf stf +# application/vnd.wv.csp+wbxml +# application/vnd.wv.csp+xml +# application/vnd.wv.ssp+xml +# application/vnd.xacml+json +application/vnd.xara xar +application/vnd.xfdl xfdl +# application/vnd.xfdl.webform +# application/vnd.xmi+xml +# application/vnd.xmpie.cpkg +# application/vnd.xmpie.dpkg +# application/vnd.xmpie.plan +# application/vnd.xmpie.ppkg +# application/vnd.xmpie.xlim +application/vnd.yamaha.hv-dic hvd +application/vnd.yamaha.hv-script hvs +application/vnd.yamaha.hv-voice hvp +application/vnd.yamaha.openscoreformat osf +application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg +# application/vnd.yamaha.remote-setup +application/vnd.yamaha.smaf-audio saf +application/vnd.yamaha.smaf-phrase spf +# application/vnd.yamaha.through-ngn +# application/vnd.yamaha.tunnel-udpencap +# application/vnd.yaoweme +application/vnd.yellowriver-custom-menu cmp +application/vnd.zul zir zirz +application/vnd.zzazz.deck+xml zaz +application/voicexml+xml vxml +# application/vq-rtcpxr +# application/watcherinfo+xml +# application/whoispp-query +# application/whoispp-response +application/widget wgt +application/winhlp hlp +# application/wita +# application/wordperfect5.1 +application/wsdl+xml wsdl +application/wspolicy+xml wspolicy +application/x-7z-compressed 7z +application/x-abiword abw +application/x-ace-compressed ace +# application/x-amf +application/x-apple-diskimage dmg +application/x-authorware-bin aab x32 u32 vox +application/x-authorware-map aam +application/x-authorware-seg aas +application/x-bcpio bcpio +application/x-bittorrent torrent +application/x-blorb blb blorb +application/x-bzip bz +application/x-bzip2 bz2 boz +application/x-cbr cbr cba cbt cbz cb7 +application/x-cdlink vcd +application/x-cfs-compressed cfs +application/x-chat chat +application/x-chess-pgn pgn +# application/x-compress +application/x-conference nsc +application/x-cpio cpio +application/x-csh csh +application/x-debian-package deb udeb +application/x-dgc-compressed dgc +application/x-director dir dcr dxr cst cct cxt w3d fgd swa +application/x-doom wad +application/x-dtbncx+xml ncx +application/x-dtbook+xml dtb +application/x-dtbresource+xml res +application/x-dvi dvi +application/x-envoy evy +application/x-eva eva +application/x-font-bdf bdf +# application/x-font-dos +# application/x-font-framemaker +application/x-font-ghostscript gsf +# application/x-font-libgrx +application/x-font-linux-psf psf +application/x-font-pcf pcf +application/x-font-snf snf +# application/x-font-speedo +# application/x-font-sunos-news +application/x-font-type1 pfa pfb pfm afm +# application/x-font-vfont +application/x-freearc arc +application/x-futuresplash spl +application/x-gca-compressed gca +application/x-glulx ulx +application/x-gnumeric gnumeric +application/x-gramps-xml gramps +application/x-gtar gtar +# application/x-gzip +application/x-hdf hdf +application/x-install-instructions install +application/x-iso9660-image iso +application/x-java-jnlp-file jnlp +application/x-latex latex +application/x-lzh-compressed lzh lha +application/x-mie mie +application/x-mobipocket-ebook prc mobi +application/x-ms-application application +application/x-ms-shortcut lnk +application/x-ms-wmd wmd +application/x-ms-wmz wmz +application/x-ms-xbap xbap +application/x-msaccess mdb +application/x-msbinder obd +application/x-mscardfile crd +application/x-msclip clp +application/x-msdownload exe dll com bat msi +application/x-msmediaview mvb m13 m14 +application/x-msmetafile wmf wmz emf emz +application/x-msmoney mny +application/x-mspublisher pub +application/x-msschedule scd +application/x-msterminal trm +application/x-mswrite wri +application/x-netcdf nc cdf +application/x-nzb nzb +application/x-pkcs12 p12 pfx +application/x-pkcs7-certificates p7b spc +application/x-pkcs7-certreqresp p7r +application/x-rar-compressed rar +application/x-research-info-systems ris +application/x-sh sh +application/x-shar shar +application/x-shockwave-flash swf +application/x-silverlight-app xap +application/x-sql sql +application/x-stuffit sit +application/x-stuffitx sitx +application/x-subrip srt +application/x-sv4cpio sv4cpio +application/x-sv4crc sv4crc +application/x-t3vm-image t3 +application/x-tads gam +application/x-tar tar +application/x-tcl tcl +application/x-tex tex +application/x-tex-tfm tfm +application/x-texinfo texinfo texi +application/x-tgif obj +application/x-ustar ustar +application/x-wais-source src +# application/x-www-form-urlencoded +application/x-x509-ca-cert der crt +application/x-xfig fig +application/x-xliff+xml xlf +application/x-xpinstall xpi +application/x-xz xz +application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8 +# application/x400-bp +# application/xacml+xml +application/xaml+xml xaml +# application/xcap-att+xml +# application/xcap-caps+xml +application/xcap-diff+xml xdf +# application/xcap-el+xml +# application/xcap-error+xml +# application/xcap-ns+xml +# application/xcon-conference-info+xml +# application/xcon-conference-info-diff+xml +application/xenc+xml xenc +application/xhtml+xml xhtml xht +# application/xhtml-voice+xml +application/xml xml xsl +application/xml-dtd dtd +# application/xml-external-parsed-entity +# application/xml-patch+xml +# application/xmpp+xml +application/xop+xml xop +application/xproc+xml xpl +application/xslt+xml xslt +application/xspf+xml xspf +application/xv+xml mxml xhvml xvml xvm +application/yang yang +application/yin+xml yin +application/zip zip +# application/zlib +# audio/1d-interleaved-parityfec +# audio/32kadpcm +# audio/3gpp +# audio/3gpp2 +# audio/ac3 +audio/adpcm adp +# audio/amr +# audio/amr-wb +# audio/amr-wb+ +# audio/aptx +# audio/asc +# audio/atrac-advanced-lossless +# audio/atrac-x +# audio/atrac3 +audio/basic au snd +# audio/bv16 +# audio/bv32 +# audio/clearmode +# audio/cn +# audio/dat12 +# audio/dls +# audio/dsr-es201108 +# audio/dsr-es202050 +# audio/dsr-es202211 +# audio/dsr-es202212 +# audio/dv +# audio/dvi4 +# audio/eac3 +# audio/encaprtp +# audio/evrc +# audio/evrc-qcp +# audio/evrc0 +# audio/evrc1 +# audio/evrcb +# audio/evrcb0 +# audio/evrcb1 +# audio/evrcnw +# audio/evrcnw0 +# audio/evrcnw1 +# audio/evrcwb +# audio/evrcwb0 +# audio/evrcwb1 +# audio/evs +# audio/example +# audio/fwdred +# audio/g711-0 +# audio/g719 +# audio/g722 +# audio/g7221 +# audio/g723 +# audio/g726-16 +# audio/g726-24 +# audio/g726-32 +# audio/g726-40 +# audio/g728 +# audio/g729 +# audio/g7291 +# audio/g729d +# audio/g729e +# audio/gsm +# audio/gsm-efr +# audio/gsm-hr-08 +# audio/ilbc +# audio/ip-mr_v2.5 +# audio/isac +# audio/l16 +# audio/l20 +# audio/l24 +# audio/l8 +# audio/lpc +audio/midi mid midi kar rmi +# audio/mobile-xmf +audio/mp4 m4a mp4a +# audio/mp4a-latm +# audio/mpa +# audio/mpa-robust +audio/mpeg mpga mp2 mp2a mp3 m2a m3a +# audio/mpeg4-generic +# audio/musepack +audio/ogg oga ogg spx +# audio/opus +# audio/parityfec +# audio/pcma +# audio/pcma-wb +# audio/pcmu +# audio/pcmu-wb +# audio/prs.sid +# audio/qcelp +# audio/raptorfec +# audio/red +# audio/rtp-enc-aescm128 +# audio/rtp-midi +# audio/rtploopback +# audio/rtx +audio/s3m s3m +audio/silk sil +# audio/smv +# audio/smv-qcp +# audio/smv0 +# audio/sp-midi +# audio/speex +# audio/t140c +# audio/t38 +# audio/telephone-event +# audio/tone +# audio/uemclip +# audio/ulpfec +# audio/vdvi +# audio/vmr-wb +# audio/vnd.3gpp.iufp +# audio/vnd.4sb +# audio/vnd.audiokoz +# audio/vnd.celp +# audio/vnd.cisco.nse +# audio/vnd.cmles.radio-events +# audio/vnd.cns.anp1 +# audio/vnd.cns.inf1 +audio/vnd.dece.audio uva uvva +audio/vnd.digital-winds eol +# audio/vnd.dlna.adts +# audio/vnd.dolby.heaac.1 +# audio/vnd.dolby.heaac.2 +# audio/vnd.dolby.mlp +# audio/vnd.dolby.mps +# audio/vnd.dolby.pl2 +# audio/vnd.dolby.pl2x +# audio/vnd.dolby.pl2z +# audio/vnd.dolby.pulse.1 +audio/vnd.dra dra +audio/vnd.dts dts +audio/vnd.dts.hd dtshd +# audio/vnd.dvb.file +# audio/vnd.everad.plj +# audio/vnd.hns.audio +audio/vnd.lucent.voice lvp +audio/vnd.ms-playready.media.pya pya +# audio/vnd.nokia.mobile-xmf +# audio/vnd.nortel.vbk +audio/vnd.nuera.ecelp4800 ecelp4800 +audio/vnd.nuera.ecelp7470 ecelp7470 +audio/vnd.nuera.ecelp9600 ecelp9600 +# audio/vnd.octel.sbc +# audio/vnd.qcelp +# audio/vnd.rhetorex.32kadpcm +audio/vnd.rip rip +# audio/vnd.sealedmedia.softseal.mpeg +# audio/vnd.vmx.cvsd +# audio/vorbis +# audio/vorbis-config +audio/webm weba +audio/x-aac aac +audio/x-aiff aif aiff aifc +audio/x-caf caf +audio/x-flac flac +audio/x-matroska mka +audio/x-mpegurl m3u +audio/x-ms-wax wax +audio/x-ms-wma wma +audio/x-pn-realaudio ram ra +audio/x-pn-realaudio-plugin rmp +# audio/x-tta +audio/x-wav wav +audio/xm xm +chemical/x-cdx cdx +chemical/x-cif cif +chemical/x-cmdf cmdf +chemical/x-cml cml +chemical/x-csml csml +# chemical/x-pdb +chemical/x-xyz xyz +font/collection ttc +font/otf otf +# font/sfnt +font/ttf ttf +font/woff woff +font/woff2 woff2 +image/bmp bmp +image/cgm cgm +# image/dicom-rle +# image/emf +# image/example +# image/fits +image/g3fax g3 +image/gif gif +image/ief ief +# image/jls +# image/jp2 +image/jpeg jpeg jpg jpe +# image/jpm +# image/jpx +image/ktx ktx +# image/naplps +image/png png +image/prs.btif btif +# image/prs.pti +# image/pwg-raster +image/sgi sgi +image/svg+xml svg svgz +# image/t38 +image/tiff tiff tif +# image/tiff-fx +image/vnd.adobe.photoshop psd +# image/vnd.airzip.accelerator.azv +# image/vnd.cns.inf2 +image/vnd.dece.graphic uvi uvvi uvg uvvg +image/vnd.djvu djvu djv +image/vnd.dvb.subtitle sub +image/vnd.dwg dwg +image/vnd.dxf dxf +image/vnd.fastbidsheet fbs +image/vnd.fpx fpx +image/vnd.fst fst +image/vnd.fujixerox.edmics-mmr mmr +image/vnd.fujixerox.edmics-rlc rlc +# image/vnd.globalgraphics.pgb +# image/vnd.microsoft.icon +# image/vnd.mix +# image/vnd.mozilla.apng +image/vnd.ms-modi mdi +image/vnd.ms-photo wdp +image/vnd.net-fpx npx +# image/vnd.radiance +# image/vnd.sealed.png +# image/vnd.sealedmedia.softseal.gif +# image/vnd.sealedmedia.softseal.jpg +# image/vnd.svf +# image/vnd.tencent.tap +# image/vnd.valve.source.texture +image/vnd.wap.wbmp wbmp +image/vnd.xiff xif +# image/vnd.zbrush.pcx +image/webp webp +# image/wmf +image/x-3ds 3ds +image/x-cmu-raster ras +image/x-cmx cmx +image/x-freehand fh fhc fh4 fh5 fh7 +image/x-icon ico +image/x-mrsid-image sid +image/x-pcx pcx +image/x-pict pic pct +image/x-portable-anymap pnm +image/x-portable-bitmap pbm +image/x-portable-graymap pgm +image/x-portable-pixmap ppm +image/x-rgb rgb +image/x-tga tga +image/x-xbitmap xbm +image/x-xpixmap xpm +image/x-xwindowdump xwd +# message/cpim +# message/delivery-status +# message/disposition-notification +# message/example +# message/external-body +# message/feedback-report +# message/global +# message/global-delivery-status +# message/global-disposition-notification +# message/global-headers +# message/http +# message/imdn+xml +# message/news +# message/partial +message/rfc822 eml mime +# message/s-http +# message/sip +# message/sipfrag +# message/tracking-status +# message/vnd.si.simp +# message/vnd.wfa.wsc +# model/example +# model/gltf+json +model/iges igs iges +model/mesh msh mesh silo +model/vnd.collada+xml dae +model/vnd.dwf dwf +# model/vnd.flatland.3dml +model/vnd.gdl gdl +# model/vnd.gs-gdl +# model/vnd.gs.gdl +model/vnd.gtw gtw +# model/vnd.moml+xml +model/vnd.mts mts +# model/vnd.opengex +# model/vnd.parasolid.transmit.binary +# model/vnd.parasolid.transmit.text +# model/vnd.rosette.annotated-data-model +# model/vnd.valve.source.compiled-map +model/vnd.vtu vtu +model/vrml wrl vrml +model/x3d+binary x3db x3dbz +# model/x3d+fastinfoset +model/x3d+vrml x3dv x3dvz +model/x3d+xml x3d x3dz +# model/x3d-vrml +# multipart/alternative +# multipart/appledouble +# multipart/byteranges +# multipart/digest +# multipart/encrypted +# multipart/example +# multipart/form-data +# multipart/header-set +# multipart/mixed +# multipart/parallel +# multipart/related +# multipart/report +# multipart/signed +# multipart/voice-message +# multipart/x-mixed-replace +# text/1d-interleaved-parityfec +text/cache-manifest appcache +text/calendar ics ifb +text/css css +text/csv csv +# text/csv-schema +# text/directory +# text/dns +# text/ecmascript +# text/encaprtp +# text/enriched +# text/example +# text/fwdred +# text/grammar-ref-list +text/html html htm +# text/javascript +# text/jcr-cnd +# text/markdown +# text/mizar +text/n3 n3 +# text/parameters +# text/parityfec +text/plain txt text conf def list log in +# text/provenance-notation +# text/prs.fallenstein.rst +text/prs.lines.tag dsc +# text/prs.prop.logic +# text/raptorfec +# text/red +# text/rfc822-headers +text/richtext rtx +# text/rtf +# text/rtp-enc-aescm128 +# text/rtploopback +# text/rtx +text/sgml sgml sgm +# text/t140 +text/tab-separated-values tsv +text/troff t tr roff man me ms +text/turtle ttl +# text/ulpfec +text/uri-list uri uris urls +text/vcard vcard +# text/vnd.a +# text/vnd.abc +text/vnd.curl curl +text/vnd.curl.dcurl dcurl +text/vnd.curl.mcurl mcurl +text/vnd.curl.scurl scurl +# text/vnd.debian.copyright +# text/vnd.dmclientscript +text/vnd.dvb.subtitle sub +# text/vnd.esmertec.theme-descriptor +text/vnd.fly fly +text/vnd.fmi.flexstor flx +text/vnd.graphviz gv +text/vnd.in3d.3dml 3dml +text/vnd.in3d.spot spot +# text/vnd.iptc.newsml +# text/vnd.iptc.nitf +# text/vnd.latex-z +# text/vnd.motorola.reflex +# text/vnd.ms-mediapackage +# text/vnd.net2phone.commcenter.command +# text/vnd.radisys.msml-basic-layout +# text/vnd.si.uricatalogue +text/vnd.sun.j2me.app-descriptor jad +# text/vnd.trolltech.linguist +# text/vnd.wap.si +# text/vnd.wap.sl +text/vnd.wap.wml wml +text/vnd.wap.wmlscript wmls +text/x-asm s asm +text/x-c c cc cxx cpp h hh dic +text/x-fortran f for f77 f90 +text/x-java-source java +text/x-nfo nfo +text/x-opml opml +text/x-pascal p pas +text/x-setext etx +text/x-sfv sfv +text/x-uuencode uu +text/x-vcalendar vcs +text/x-vcard vcf +# text/xml +# text/xml-external-parsed-entity +# video/1d-interleaved-parityfec +video/3gpp 3gp +# video/3gpp-tt +video/3gpp2 3g2 +# video/bmpeg +# video/bt656 +# video/celb +# video/dv +# video/encaprtp +# video/example +video/h261 h261 +video/h263 h263 +# video/h263-1998 +# video/h263-2000 +video/h264 h264 +# video/h264-rcdo +# video/h264-svc +# video/h265 +# video/iso.segment +video/jpeg jpgv +# video/jpeg2000 +video/jpm jpm jpgm +video/mj2 mj2 mjp2 +# video/mp1s +# video/mp2p +# video/mp2t +video/mp4 mp4 mp4v mpg4 +# video/mp4v-es +video/mpeg mpeg mpg mpe m1v m2v +# video/mpeg4-generic +# video/mpv +# video/nv +video/ogg ogv +# video/parityfec +# video/pointer +video/quicktime qt mov +# video/raptorfec +# video/raw +# video/rtp-enc-aescm128 +# video/rtploopback +# video/rtx +# video/smpte292m +# video/ulpfec +# video/vc1 +# video/vnd.cctv +video/vnd.dece.hd uvh uvvh +video/vnd.dece.mobile uvm uvvm +# video/vnd.dece.mp4 +video/vnd.dece.pd uvp uvvp +video/vnd.dece.sd uvs uvvs +video/vnd.dece.video uvv uvvv +# video/vnd.directv.mpeg +# video/vnd.directv.mpeg-tts +# video/vnd.dlna.mpeg-tts +video/vnd.dvb.file dvb +video/vnd.fvt fvt +# video/vnd.hns.video +# video/vnd.iptvforum.1dparityfec-1010 +# video/vnd.iptvforum.1dparityfec-2005 +# video/vnd.iptvforum.2dparityfec-1010 +# video/vnd.iptvforum.2dparityfec-2005 +# video/vnd.iptvforum.ttsavc +# video/vnd.iptvforum.ttsmpeg2 +# video/vnd.motorola.video +# video/vnd.motorola.videop +video/vnd.mpegurl mxu m4u +video/vnd.ms-playready.media.pyv pyv +# video/vnd.nokia.interleaved-multimedia +# video/vnd.nokia.videovoip +# video/vnd.objectvideo +# video/vnd.radgamettools.bink +# video/vnd.radgamettools.smacker +# video/vnd.sealed.mpeg1 +# video/vnd.sealed.mpeg4 +# video/vnd.sealed.swf +# video/vnd.sealedmedia.softseal.mov +video/vnd.uvvu.mp4 uvu uvvu +video/vnd.vivo viv +# video/vp8 +video/webm webm +video/x-f4v f4v +video/x-fli fli +video/x-flv flv +video/x-m4v m4v +video/x-matroska mkv mk3d mks +video/x-mng mng +video/x-ms-asf asf asx +video/x-ms-vob vob +video/x-ms-wm wm +video/x-ms-wmv wmv +video/x-ms-wmx wmx +video/x-ms-wvx wvx +video/x-msvideo avi +video/x-sgi-movie movie +video/x-smv smv +x-conference/x-cooltalk ice From e6aa20d016aa0aa37872674c325af52885180019 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 02:57:13 +0000 Subject: [PATCH 611/701] build(deps): bump the amazon-s3 group with 2 updates Bumps the amazon-s3 group with 2 updates: software.amazon.awssdk:s3 and [software.amazon.awssdk.crt:aws-crt](https://github.com/awslabs/aws-crt-java). Updates `software.amazon.awssdk:s3` from 2.42.26 to 2.42.40 Updates `software.amazon.awssdk.crt:aws-crt` from 0.44.0 to 0.45.1 - [Release notes](https://github.com/awslabs/aws-crt-java/releases) - [Commits](https://github.com/awslabs/aws-crt-java/compare/v0.44.0...v0.45.1) --- updated-dependencies: - dependency-name: software.amazon.awssdk:s3 dependency-version: 2.42.40 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: amazon-s3 - dependency-name: software.amazon.awssdk.crt:aws-crt dependency-version: 0.45.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: amazon-s3 ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 31ac0523f814..824aa43b00e1 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -715,7 +715,7 @@ software.amazon.awssdk s3 - 2.42.26 + 2.42.40 software.amazon.awssdk @@ -731,7 +731,7 @@ software.amazon.awssdk.crt aws-crt - 0.44.0 + 0.45.1 17 - 6.2.17 - 3.3.6 - 3.5.13 - 6.5.9 + 6.2.18 + 3.3.7 + 3.5.14 + 6.5.10 6.4.10.Final 8.0.3.Final 42.7.10 From bcdb40ed540c99f9d283ea4d873282a7701d82a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:01:03 +0000 Subject: [PATCH 614/701] build(deps): bump org.apache.ant:ant from 1.10.16 to 1.10.17 Bumps org.apache.ant:ant from 1.10.16 to 1.10.17. --- updated-dependencies: - dependency-name: org.apache.ant:ant dependency-version: 1.10.17 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dfa193886b17..325f46befbf5 100644 --- a/pom.xml +++ b/pom.xml @@ -1413,7 +1413,7 @@ org.apache.ant ant - 1.10.16 + 1.10.17 org.apache.jena From 4c4dd980966bdf9b80fcf39d4c675dacfd7fa6a9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:02:01 +0000 Subject: [PATCH 615/701] build(deps): bump org.ehcache:ehcache from 3.11.1 to 3.12.0 Bumps [org.ehcache:ehcache](https://github.com/ehcache/ehcache3) from 3.11.1 to 3.12.0. - [Release notes](https://github.com/ehcache/ehcache3/releases) - [Commits](https://github.com/ehcache/ehcache3/compare/v3.11.1...v3.12.0) --- updated-dependencies: - dependency-name: org.ehcache:ehcache dependency-version: 3.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dfa193886b17..f8cd4b2e99f6 100644 --- a/pom.xml +++ b/pom.xml @@ -29,7 +29,7 @@ 10.22.0 8.11.4 - 3.11.1 + 3.12.0 2.42.0 2.21.2 From fb4800c01021d6eda22a5c2fe24661ab03821cce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 03:02:08 +0000 Subject: [PATCH 616/701] build(deps): bump xom:xom from 1.3.9 to 1.4.0 Bumps [xom:xom](https://github.com/elharo/xom) from 1.3.9 to 1.4.0. - [Release notes](https://github.com/elharo/xom/releases) - [Commits](https://github.com/elharo/xom/compare/v1.3.9...v1.4.0) --- updated-dependencies: - dependency-name: xom:xom dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dspace-sword/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index bfb54a7db5a1..102ee728569f 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -72,7 +72,7 @@ xom xom - 1.3.9 + 1.4.0 diff --git a/pom.xml b/pom.xml index dfa193886b17..e7fe302fa4f5 100644 --- a/pom.xml +++ b/pom.xml @@ -1797,7 +1797,7 @@ xom xom - 1.3.9 + 1.4.0 xml-apis From 0af457efc73ccc8d156e9c0559da5d9ecd9ddcef Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Mon, 4 May 2026 15:21:09 +0200 Subject: [PATCH 617/701] DSpace#12402 Fix duplicate submit.type-bind.field refs in dspace.cfg Removed the second instance and kept the first, grouped with other submission changes. Uncommented the single remaining instance as that is required for the rest.cfg config property access (cherry picked from commit 8b797fde5a05ddc9678916c16f4629f5bb4da41a) --- dspace/config/dspace.cfg | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 53d1ee039ec3..09ce15137cf0 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1003,9 +1003,11 @@ metadata.hide.person.email = true #webui.submit.upload.required = true # Which field should be used for type-bind -# Defaults to 'dc.type'; If changing this value, you must also update the related -# dspace-angular environment configuration property submission.typeBind.field -#submit.type-bind.field = dc.type +# If changing this value, you must also update the related +# dspace-angular environment configuration property submission.typeBind.field. +# This property should not be left commented out, or the REST configuration +# exposure will fail +submit.type-bind.field = dc.type #### Creative Commons settings ###### @@ -1564,12 +1566,6 @@ request.item.helpdesk.override = false # Setting it to "false" results in a silent rejection. request.item.reject.email = true -#------------------------------------------------------------------# -#------------------SUBMISSION CONFIGURATION------------------------# -#------------------------------------------------------------------# -# Field to use for type binding, default dc.type -submit.type-bind.field = dc.type - #---------------------------------------------------------------# #----------SOLR DATABASE RESYNC SCRIPT CONFIGURATION------------# #---------------------------------------------------------------# From 9b2da997c3c04b3edc2fdba244868e62baaaeef8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 02:56:50 +0000 Subject: [PATCH 618/701] build(deps-dev): bump the test-tools group with 3 updates Bumps the test-tools group with 3 updates: com.adobe.testing:s3mock-testcontainers, [org.testcontainers:testcontainers](https://github.com/testcontainers/testcontainers-java) and [org.apache.httpcomponents.client5:httpclient5](https://github.com/apache/httpcomponents-client). Updates `com.adobe.testing:s3mock-testcontainers` from 4.11.0 to 4.12.4 Updates `org.testcontainers:testcontainers` from 2.0.4 to 2.0.5 - [Release notes](https://github.com/testcontainers/testcontainers-java/releases) - [Changelog](https://github.com/testcontainers/testcontainers-java/blob/main/CHANGELOG.md) - [Commits](https://github.com/testcontainers/testcontainers-java/compare/2.0.4...2.0.5) Updates `org.apache.httpcomponents.client5:httpclient5` from 5.6 to 5.6.1 - [Changelog](https://github.com/apache/httpcomponents-client/blob/rel/v5.6.1/RELEASE_NOTES.txt) - [Commits](https://github.com/apache/httpcomponents-client/compare/rel/v5.6...rel/v5.6.1) --- updated-dependencies: - dependency-name: com.adobe.testing:s3mock-testcontainers dependency-version: 4.12.4 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: test-tools - dependency-name: org.testcontainers:testcontainers dependency-version: 2.0.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: test-tools - dependency-name: org.apache.httpcomponents.client5:httpclient5 dependency-version: 5.6.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: test-tools ... Signed-off-by: dependabot[bot] --- dspace-api/pom.xml | 4 ++-- dspace-server-webapp/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 31ac0523f814..4cb8ed92b6d9 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -847,7 +847,7 @@ com.adobe.testing s3mock-testcontainers - 4.11.0 + 4.12.4 test @@ -862,7 +862,7 @@ org.testcontainers testcontainers - 2.0.4 + 2.0.5 test diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index f69507d2cee3..64c2884c5f7f 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -588,7 +588,7 @@ org.apache.httpcomponents.client5 httpclient5 - 5.6 + 5.6.1 test From 2213759d9b6d158c7c38349e62001b7ff3b120af Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 6 May 2026 09:07:29 +0300 Subject: [PATCH 619/701] pom.xml: bump postgresql.driver.version to 42.7.11 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 69d8755cc2cc..cee8a75c860f 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ 6.5.10 6.4.10.Final 8.0.3.Final - 42.7.10 + 42.7.11 10.22.0 8.11.4 From 049290718931e8b4074f1dd9974bae66254a7189 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 6 May 2026 09:08:06 +0300 Subject: [PATCH 620/701] pom.xml: remove org.checkerframework:checker-qual This was only pinned to avoid dependency convergence issues between org.postgresql:postgresql and org.apache.solr:solr-core, but as of version 42.7.11, org.postgresql:postgresql no longer depends on it. --- pom.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pom.xml b/pom.xml index cee8a75c860f..34143f5060ce 100644 --- a/pom.xml +++ b/pom.xml @@ -1373,13 +1373,6 @@ ${asm.version} - - - org.checkerframework - checker-qual - 3.54.0 - - From 88a2f334703301019ec5c9ce24419ade26e1d9bb Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 6 May 2026 09:08:46 +0300 Subject: [PATCH 621/701] pom.xml: update comment about org.checkerframework:checker-qual Both com.google.guava:guava and org.apache.solr:solr-core depend on org.checkerframework:checker-qual, but org.postgresql:postgresql no longer does. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 34143f5060ce..008e7ba00be1 100644 --- a/pom.xml +++ b/pom.xml @@ -1780,7 +1780,7 @@ guava 32.1.3-jre - + org.checkerframework checker-qual From 7b6344b7511a6243e375deb31d77d2b4638389e5 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 May 2026 11:14:59 -0500 Subject: [PATCH 622/701] Replace several javax.annotation dependencies with their jakarta.annotation equivalent --- .../rest/repository/WorkflowItemCollectionLinkRepository.java | 2 +- .../app/rest/repository/WorkflowItemItemLinkRepository.java | 2 +- .../rest/repository/WorkflowItemSubmitterLinkRepository.java | 2 +- .../rest/repository/WorkspaceItemCollectionLinkRepository.java | 2 +- .../app/rest/repository/WorkspaceItemItemLinkRepository.java | 2 +- .../rest/repository/WorkspaceItemSubmitterLinkRepository.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemCollectionLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemCollectionLinkRepository.java index 09c7cc4c0f12..6d7438983e97 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemCollectionLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemCollectionLinkRepository.java @@ -8,8 +8,8 @@ package org.dspace.app.rest.repository; import java.sql.SQLException; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.WorkflowItemRest; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemItemLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemItemLinkRepository.java index 96b09c8d64ac..6d5393ed0134 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemItemLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemItemLinkRepository.java @@ -8,8 +8,8 @@ package org.dspace.app.rest.repository; import java.sql.SQLException; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.WorkflowItemRest; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemSubmitterLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemSubmitterLinkRepository.java index 9fe8e29bfa6b..efae3d8b0936 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemSubmitterLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemSubmitterLinkRepository.java @@ -8,8 +8,8 @@ package org.dspace.app.rest.repository; import java.sql.SQLException; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.EPersonRest; import org.dspace.app.rest.model.WorkflowItemRest; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemCollectionLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemCollectionLinkRepository.java index a3ad57c31c72..d362b15c5103 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemCollectionLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemCollectionLinkRepository.java @@ -8,8 +8,8 @@ package org.dspace.app.rest.repository; import java.sql.SQLException; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.WorkspaceItemRest; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemItemLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemItemLinkRepository.java index 0eb4e1b27dcb..2e4185c8f5dc 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemItemLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemItemLinkRepository.java @@ -8,8 +8,8 @@ package org.dspace.app.rest.repository; import java.sql.SQLException; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.WorkspaceItemRest; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemSubmitterLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemSubmitterLinkRepository.java index 707ab7e13d5e..73c1acd51ce2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemSubmitterLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemSubmitterLinkRepository.java @@ -8,8 +8,8 @@ package org.dspace.app.rest.repository; import java.sql.SQLException; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.EPersonRest; import org.dspace.app.rest.model.WorkspaceItemRest; From 88b24105b4527203985027fdfb4cc447b479066f Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 6 May 2026 11:25:47 -0500 Subject: [PATCH 623/701] Updated our banned dependency list to include various Javax dependencies. Exclude javax.annotation from cnri-servlet-container where it was still being brought in. --- dspace-api/pom.xml | 5 +++++ pom.xml | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 4cb8ed92b6d9..fb417d87c713 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -414,6 +414,11 @@ org.bouncycastle bcprov-jdk15on + + + javax.annotation + javax.annotation-api + diff --git a/pom.xml b/pom.xml index 2f148182f447..cb6dc0d5522e 100644 --- a/pom.xml +++ b/pom.xml @@ -131,7 +131,16 @@ + log4j:log4j + + javax.annotation:javax.annotation-api + javax.inject:javax.inject + javax.mail:mail + com.sun.mail:javax.mail + javax.persistence:javax.persistence-api + javax.servlet:servlet-api + javax.validation:validation-api From 91d3e9abfd50a4b70792bca49dce2385f0ec6df2 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Mon, 4 May 2026 16:46:07 +0300 Subject: [PATCH 624/701] pom.xml: bump com.google.guava:guava to 33.6.0-jre (cherry picked from commit f82f3f3aaea7dba9ff2186bb84391f9a2ec11323) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cb6dc0d5522e..2b7343a62a5a 100644 --- a/pom.xml +++ b/pom.xml @@ -1787,7 +1787,7 @@ com.google.guava guava - 32.1.3-jre + 33.6.0-jre From cf835c6a9a3f1acce33de80a1c44d83af0a7f27f Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Thu, 7 May 2026 09:16:20 +0300 Subject: [PATCH 625/701] pom.xml: ignore micrometer dependencies Both Spring LDAP and the main Spring framework pull in different versions of micrometer, which causes dependency convergence. As none of our own code actually uses micrometer, we should not be pinning any version. Spring framework currently pulls in a newer version of micrometer than Spring LDAP, and all tests are passing with that version, so we can prefer it over the one pulled in by Spring LDAP. (cherry picked from commit 1b95540d7b9a28c771f0770311925f55c3887644) --- pom.xml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pom.xml b/pom.xml index 348b4057607c..1f54b23458e0 100644 --- a/pom.xml +++ b/pom.xml @@ -1225,17 +1225,19 @@ org.springframework.ldap spring-ldap-core ${spring-ldap.version} - - - - io.micrometer - micrometer-core - 1.14.14 - - - io.micrometer - micrometer-observation - 1.14.14 + + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-observation + + From 93078c603e771499ac0500aa3a6f483b404665cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 20:15:38 +0000 Subject: [PATCH 626/701] build(deps): bump com.google.code.gson:gson from 2.13.2 to 2.14.0 Bumps [com.google.code.gson:gson](https://github.com/google/gson) from 2.13.2 to 2.14.0. - [Release notes](https://github.com/google/gson/releases) - [Changelog](https://github.com/google/gson/blob/main/CHANGELOG.md) - [Commits](https://github.com/google/gson/compare/gson-parent-2.13.2...gson-parent-2.14.0) --- updated-dependencies: - dependency-name: com.google.code.gson:gson dependency-version: 2.14.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7975af7315f9..6fafd4f53242 100644 --- a/pom.xml +++ b/pom.xml @@ -1389,7 +1389,7 @@ com.google.code.gson gson - 2.13.2 + 2.14.0 - 2.21.2 + 2.21.3 2.21 2.1.1 4.0.5 From 1699d9bf507f5c37a21948c0b6e293eb19c08988 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 22:11:57 +0000 Subject: [PATCH 629/701] build(deps): bump org.glassfish.jaxb:jaxb-runtime in the jakarta group Bumps the jakarta group with 1 update: org.glassfish.jaxb:jaxb-runtime. Updates `org.glassfish.jaxb:jaxb-runtime` from 4.0.7 to 4.0.8 --- updated-dependencies: - dependency-name: org.glassfish.jaxb:jaxb-runtime dependency-version: 4.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: jakarta ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7975af7315f9..64cca5727839 100644 --- a/pom.xml +++ b/pom.xml @@ -36,7 +36,7 @@ 2.21 2.1.1 4.0.5 - 4.0.7 + 4.0.8 1.1.1 9.4.58.v20250814 From 791b396dcec7cd10c6f69db70d79aa39a2fed1e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 22:13:06 +0000 Subject: [PATCH 630/701] build(deps): bump xom:xom from 1.4.0 to 1.4.1 Bumps [xom:xom](https://github.com/elharo/xom) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/elharo/xom/releases) - [Commits](https://github.com/elharo/xom/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: xom:xom dependency-version: 1.4.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dspace-sword/pom.xml | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 102ee728569f..22dc03788a6e 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -72,7 +72,7 @@ xom xom - 1.4.0 + 1.4.1 diff --git a/pom.xml b/pom.xml index 7975af7315f9..db848bbdf8a7 100644 --- a/pom.xml +++ b/pom.xml @@ -1801,7 +1801,7 @@ xom xom - 1.4.0 + 1.4.1 xml-apis From 338c72b60bc6ece73a4ef4785b89f0015ffe88ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 22:13:38 +0000 Subject: [PATCH 631/701] build(deps): bump org.apache.james:apache-mime4j-core Bumps org.apache.james:apache-mime4j-core from 0.8.13 to 0.8.14. --- updated-dependencies: - dependency-name: org.apache.james:apache-mime4j-core dependency-version: 0.8.14 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7975af7315f9..f098742139e5 100644 --- a/pom.xml +++ b/pom.xml @@ -1360,7 +1360,7 @@ org.apache.james apache-mime4j-core - 0.8.13 + 0.8.14 From df0d7da1a48e78616bcfa5ad0de583d913a62cf1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 May 2026 22:13:54 +0000 Subject: [PATCH 632/701] build(deps): bump jaxen:jaxen from 2.0.0 to 2.0.1 Bumps [jaxen:jaxen](https://github.com/jaxen-xpath/jaxen) from 2.0.0 to 2.0.1. - [Release notes](https://github.com/jaxen-xpath/jaxen/releases) - [Commits](https://github.com/jaxen-xpath/jaxen/compare/v2.0.0...v2.0.1) --- updated-dependencies: - dependency-name: jaxen:jaxen dependency-version: 2.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7975af7315f9..8fcfc04316c3 100644 --- a/pom.xml +++ b/pom.xml @@ -1609,7 +1609,7 @@ jaxen jaxen - 2.0.0 + 2.0.1 org.jdom From 39a48cdac6bd8f073b1bf7b13d997c6a05f73966 Mon Sep 17 00:00:00 2001 From: Stefano Maffei <48757042+steph-ieffam@users.noreply.github.com> Date: Fri, 8 May 2026 15:55:56 +0200 Subject: [PATCH 633/701] [DURACOM-477] Fix slow loading times when caching counts for items (#12344) * [CST-25040] make cache count for items in collections/communities Asynch * [CST-25040] fix cache implementation * [CST-25040] fix cache implementation * [DURACOM-477] cleanup (cherry picked from commit 36b3f5c6cc577a8d5e0c66dd4c0787e28849b689) --- .../org/dspace/browse/ItemCountDAOSolr.java | 119 +++++------------- dspace/config/spring/api/core-services.xml | 2 +- dspace/solr/search/conf/schema.xml | 2 +- 3 files changed, 32 insertions(+), 91 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/browse/ItemCountDAOSolr.java b/dspace-api/src/main/java/org/dspace/browse/ItemCountDAOSolr.java index e4d0079fe20b..723b49d63aea 100644 --- a/dspace-api/src/main/java/org/dspace/browse/ItemCountDAOSolr.java +++ b/dspace-api/src/main/java/org/dspace/browse/ItemCountDAOSolr.java @@ -7,121 +7,62 @@ */ package org.dspace.browse; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.response.QueryResponse; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; -import org.dspace.discovery.DiscoverFacetField; -import org.dspace.discovery.DiscoverQuery; -import org.dspace.discovery.DiscoverResult; -import org.dspace.discovery.DiscoverResult.FacetResult; -import org.dspace.discovery.SearchService; -import org.dspace.discovery.SearchServiceException; -import org.dspace.discovery.configuration.DiscoveryConfigurationParameters; +import org.dspace.discovery.SolrSearchCore; import org.dspace.discovery.indexobject.IndexableItem; import org.springframework.beans.factory.annotation.Autowired; /** * Discovery (Solr) driver implementing ItemCountDAO interface to look up item - * count information in communities and collections. Caching operations are - * intentionally not implemented because Solr already is our cache. + * count information in communities and collections. + *

+ * Counts are computed by querying Solr for archived, non-withdrawn, discoverable + * items using {@code location.comm} / {@code location.coll} filters. + * The query returns only {@code numFound} (rows=0), making it very fast. */ public class ItemCountDAOSolr implements ItemCountDAO { - /** - * Log4j logger - */ - private static Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemCountDAOSolr.class); - - /** - * Hold the communities item count obtained from SOLR after the first query. This only works - * well if the ItemCountDAO lifecycle is bound to the request lifecycle as - * it is now. If we switch to a Spring-based instantiation we should mark - * this bean as prototype - **/ - private Map communitiesCount = null; - - /** - * Hold the collection item count obtained from SOLR after the first query - **/ - private Map collectionsCount = null; + private static final Logger log = LogManager.getLogger(ItemCountDAOSolr.class); - /** - * Solr search service - */ @Autowired - protected SearchService searchService; + private SolrSearchCore solrSearchCore; - /** - * Get the count of the items in the given container. - * - * @param context DSpace context - * @param dso DspaceObject - * @return count - */ @Override public int getCount(Context context, DSpaceObject dso) { - loadCount(context); - Integer val = null; + String locationFilter; if (dso instanceof Collection) { - val = collectionsCount.get(dso.getID().toString()); + locationFilter = "location.coll:" + dso.getID().toString(); } else if (dso instanceof Community) { - val = communitiesCount.get(dso.getID().toString()); - } - - if (val != null) { - return val; + locationFilter = "location.comm:" + dso.getID().toString(); } else { return 0; } - } - /** - * make sure that the counts are actually fetched from Solr (if haven't been - * cached in a Map yet) - * - * @param context DSpace Context - */ - private void loadCount(Context context) { - if (communitiesCount != null || collectionsCount != null) { - return; - } - - communitiesCount = new HashMap<>(); - collectionsCount = new HashMap<>(); - - DiscoverQuery query = new DiscoverQuery(); - query.setFacetMinCount(1); - query.addFacetField(new DiscoverFacetField("location.comm", - DiscoveryConfigurationParameters.TYPE_STANDARD, -1, - DiscoveryConfigurationParameters.SORT.COUNT)); - query.addFacetField(new DiscoverFacetField("location.coll", - DiscoveryConfigurationParameters.TYPE_STANDARD, -1, - DiscoveryConfigurationParameters.SORT.COUNT)); - query.addFilterQueries("search.resourcetype:" + IndexableItem.TYPE); // count only items - query.addFilterQueries("NOT(discoverable:false)"); // only discoverable - query.addFilterQueries("withdrawn:false"); // only not withdrawn - query.addFilterQueries("archived:true"); // only archived - query.setMaxResults(0); - - DiscoverResult sResponse; try { - sResponse = searchService.search(context, query); - List commCount = sResponse.getFacetResult("location.comm"); - List collCount = sResponse.getFacetResult("location.coll"); - for (FacetResult c : commCount) { - communitiesCount.put(c.getAsFilterQuery(), (int) c.getCount()); - } - for (FacetResult c : collCount) { - collectionsCount.put(c.getAsFilterQuery(), (int) c.getCount()); + SolrClient solr = solrSearchCore.getSolr(); + if (solr == null) { + return 0; } - } catch (SearchServiceException e) { - log.error("Could not initialize Community/Collection Item Counts from Solr: ", e); + SolrQuery query = new SolrQuery("*:*"); + query.addFilterQuery(locationFilter); + query.addFilterQuery("search.resourcetype:" + IndexableItem.TYPE); + query.addFilterQuery("NOT(discoverable:false)"); + query.addFilterQuery("withdrawn:false"); + query.addFilterQuery("archived:true"); + query.setRows(0); + QueryResponse response = solr.query(query, solrSearchCore.REQUEST_METHOD); + return (int) response.getResults().getNumFound(); + } catch (Exception e) { + log.error("Error counting items in Solr for {}: ", dso.getID(), e); } + return 0; } } diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 60ace05767be..0538d99509df 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -32,7 +32,7 @@ - + diff --git a/dspace/solr/search/conf/schema.xml b/dspace/solr/search/conf/schema.xml index 5c79519159a4..1caaf1efe1ad 100644 --- a/dspace/solr/search/conf/schema.xml +++ b/dspace/solr/search/conf/schema.xml @@ -276,7 +276,7 @@ - + From 07737d6588705be778a1d1105dd1088d47732a9b Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Thu, 16 Apr 2026 14:06:28 +0200 Subject: [PATCH 634/701] Fix SWORDv2 deposit with embargo for non-admin submitters (cherry picked from commit cf6e027e0314bc6b11f3796e360ecd4415749b54) --- .../packager/AbstractMETSIngester.java | 20 +++++++++ .../java/org/dspace/app/sword2/Swordv2IT.java | 42 ++++++++++++++++++ .../org/dspace/app/sword2/example-embargo.zip | Bin 0 -> 11982 bytes .../CollectionDepositManagerDSpace.java | 3 ++ .../sword2/SwordMETSContentIngester.java | 9 ++++ 5 files changed, 74 insertions(+) create mode 100644 dspace-server-webapp/src/test/resources/org/dspace/app/sword2/example-embargo.zip diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index 0ab5ac71cda5..68383f1d9e74 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -25,6 +25,9 @@ import org.apache.logging.log4j.Logger; import org.dspace.app.client.DSpaceHttpClientFactory; import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bitstream; import org.dspace.content.BitstreamFormat; import org.dspace.content.Bundle; @@ -127,6 +130,10 @@ public abstract class AbstractMETSIngester extends AbstractPackageIngester { protected final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + protected final AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + + /** *

* An instance of ZipMdrefManager holds the state needed to retrieve the @@ -763,11 +770,24 @@ protected void addBitstreams(Context context, Item item, bitstream.setSequenceID(Integer.parseInt(seqID)); } + // Get TYPE_SUBMISSION policies before removing them in the `manifest.crosswalkBitstream` method. + List bitstreamPolicies = + authorizeService.findPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_SUBMISSION); + // crosswalk this bitstream's administrative metadata located in // METS manifest (or referenced externally) manifest.crosswalkBitstream(context, params, bitstream, mfileID, mdRefCallback); + // Only add the saved TYPE_SUBMISSION policies if the crosswalk actually removed them to prevent duplicates. + if (!bitstreamPolicies.isEmpty()) { + List remainingSubmissionPolicies = + authorizeService.findPoliciesByDSOAndType(context, bitstream, ResourcePolicy.TYPE_SUBMISSION); + if (remainingSubmissionPolicies.isEmpty()) { + authorizeService.addPolicies(context, bitstreamPolicies, bitstream); + } + } + // is this the primary bitstream? if (primaryID != null && mfileID.equals(primaryID)) { bundle.setPrimaryBitstreamID(bitstream); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java index 562f7e19edcb..f159c7d1c497 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java @@ -190,6 +190,48 @@ public void mediaResourceUnauthorizedTest() throws Exception { assertEquals(response.getStatusCode(), HttpStatus.UNAUTHORIZED); } + /** + * There should not be any Internal Server/Authorization error when uploading a new Item with embargo + * The embargo is defined in the `mets.xml` of the `example-embargo.zip` file + */ + @Test + public void depositItemWithEmbargo() throws Exception { + context.turnOffAuthorisationSystem(); + // Create a top level community and one Collection + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + // Make sure our Collection allows the "eperson" user to submit into it + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Test SWORDv2 Collection") + .withSubmitterGroup(eperson) + .build(); + // Above changes MUST be committed to the database for SWORDv2 to see them. + context.commit(); + context.restoreAuthSystemState(); + + // Add file + LinkedMultiValueMap multipart = new LinkedMultiValueMap<>(); + multipart.add("file", new FileSystemResource(Path.of("src", "test", "resources", + "org", "dspace", "app", "sword2", "example-embargo.zip"))); + // Add required headers + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + headers.setContentDisposition(ContentDisposition.attachment().filename("example-embargo.zip").build()); + headers.set("Packaging", "http://purl.org/net/sword/package/METSDSpaceSIP"); + headers.setAccept(List.of(MediaType.APPLICATION_ATOM_XML)); + + + // Send POST to upload Zip file via SWORD + ResponseEntity response = postResponseAsString(COLLECTION_PATH + "/" + collection.getHandle(), + eperson.getEmail(), password, + new HttpEntity<>(multipart, headers)); + + // Expect a 201 CREATED response with ATOM "entry" content returned + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + assertEquals(ATOM_ENTRY_CONTENT_TYPE, response.getHeaders().getContentType().toString()); + } + /** * This tests four different SWORDv2 actions, as these all require starting with a new deposit. * 1. Depositing a new item via SWORD (via POST /collections/[collection-uuid]) diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/sword2/example-embargo.zip b/dspace-server-webapp/src/test/resources/org/dspace/app/sword2/example-embargo.zip new file mode 100644 index 0000000000000000000000000000000000000000..ff1273679b88fb5292974a21d6a35b74ec0d42b7 GIT binary patch literal 11982 zcmZ{~V{jz^6Qv#Acw*bOor!JRwsm7pjEQaAoH&`-$;7su+$8(H-&eJ}RlD0=)u+0< z&j0h{QI>;*!TINT>tnS0q&IK}&(GqrM(|`Nr`rD^8&_>`KP0JV0T;>BQ$s~UW!Qn) zlaSC88gjNk1YtGk0hU-etiIYQv4X05&?k(WY&R=ErMbFd5PB6w$!fKF6mSVInYaD3 z4t#gcry8Md1{Pb(Gd!j)@0#dm(ci3Jm+I<=LExb##d($zrIu#7tN3n;hjD1P0HiuEQ7?-W94JP`1tsCUnKEiW*`wC zAN!daSVOSU>Cw^2$)#KNK?^=}dOM1lyUt@^O%8mVzld=OR0TUS$ zfANF-vUYzgh?`=lvA-lJ5E&s2%8L&U&P&=$U*27$x#j&cKI#5ik}ULqDo35B_T~TA zi@4Vklby!yu>W5SwpJ&hb3&ZK?kVfQ0a~)^0$Wp+?KxhS?!Oae96Vejj!yUK^LHe> z?CiD~raH%KqCCUkp97?YCVO7j!clQCO49Papu_g^S++bY%U+-C^zXV|;9jIs{v!}b=UX<5)9Kho1!cAM;>;5oLXR` zXyyJU0l8jz&3@&Xy?o^he3YAg-`gSXnioJ~Cj?^$L8RI_C6@$$pL!(nzN|DKbaBbG z+3w^Wb#QBHjcKMqx9Q>Jn*~GH@$$`q{fJ0Aa~KB$FpEL{o5Lex6n9E;MwkTquAB%R z=U6|vL8_}*y~MjmXE|IHkV+IBujVKy3<3~D&TNzQsbkpj=FJqbtBBQFmuSu|1dV)! zZs(YsB!#NIs1pEhS#s!fTW4ztia8r(y(J-=CY#i}w6D>XEX_MezI( zvPHr#IT_|F_SMPS4VKpg4@(Y_d{bG-Q@3#<4iYY`NRm5-p(uY2;~!M_N*7sPX`+vO zJ1V15OO6C&6MplDSwNsjW8#a|Z_!H!BDt|xozsj}w({S=%@>Nr<9cRS&a z^YGGF$2awm1WUJ#vttzG; z`Hiq?pI1=EBBI(@kPX!lbrBY_>l%c#3oG-Xog;1rV69)#1B6BFwCY9Qyi4#AC#IW|P_ScSmiVvDpRkj! zN;pNGmb+#nNWsjh(X!|8NQp(@u@}?rtt9)ueSMsg4?T}ZMZC)sehxefdk!KQW$^m5fI?(!GuK(Q&Vfak6GEL2u;xQ`h^m!1#X6M9-M76(%2*ToK?#XXBndmD@+x}p zUrY#rmR=!o0B29A5Pol{XzVg@D0=hed-L&IyYXwjyjdJSvHb^gV{OgQ31l<$qN0FJ=DKq&!d3m2AyUUYg~9nzp1sX@zi?Ng znF1EsDSGTpS`G`#g)fj~yNP;oDs)l`9{Iy{l95a*`)apg$nc-%6@;r%qOB&s718TY zD@C)r3hsAz%qq|_~EDJ5>-dM6XHD~RaY9Ax?= zzn5~UBnZP!kO|)018%PM6<~0`rZsclQyAI?;dpVLI&$I6V`}381s^P{3yHH`#s_G-l0GeH z9{gq(;xU=90$9in2O%v<*BcxN$U6P7f(}UHX#J4_Jt?My9aLOzSG4A$&OCB zazwHsAEB$c1qVAu(0nldv^v-iaLLbc8^bsm%m z7W$^ndvQZsGaB`r8`O#SOTmA*AE#o^tyQ2{cJi8onozjj#r?S7nrPBQ+XPX7HlHbo zN!h;zjp9`l+ARmG8OKAlF+5L7Nb{k$b}ku^D+S{(@2~_Vao&;Rxshdl0e*FFZZTsF zc}Xy_46Rb_S^k)zda?*^TyAVu4RL6u9b&8uk*wPv9QaTGnlMMy4uxD~PA_(>M%jlZXQdcK z0gG@yNrJ@FCH-T;6cL2+r3Y5gvyMu94B{Jh= z_o{(O9Q4q%hTRUJ*#kqZX&~f65%>ip`qWPWY+CHR(EbyrZ^MDc=0i%zGBGsiLvvT{ zQ9$@og?1mOK!&Vu#3Y{y@MXaSy=n9a`1(o(JuxAipo^G?@ zys~3;8DS(TT>H;_zGRXbXO+K@@^x{5MMyDiNwydJMp@CS(U2Yc2bvw)un;H*IfV8H zt3JxSKC0scLPtgw<3_4*f_!W4)^J&;gCwx(FO$r+IDM4@rCTkZ7J^PFn|*IYMb5=@ zUs*{$+2pnC&-UGIM1WR`?Co6JJ{ zSC4o>hd0A;$25;9F zoy}t9P?Tsuik+GNwnjt>vLvP5STt|0K;I-%1yRr|WcdF|a9_r{f4aAUyOYiRXs(Em z0KJ_5Q%#K@MeR;Mh?q)W+2B};17^d~Xjcn-KQ~U2<>)J~zAjnCV4MGI`pZ~$QQzZ} zP1yF#x5>$);Jf>vV*+-X2r?agzyXdL26Rx;dGow12+jczPi576Je(|1zxM=2 z9)BL7flz@~$&yPejdy4@nn?^;3_eWRY7qKT3Ao8#IK3HdIF#}vJdOR!TxP|Oj0EIv zH0)Dvz=dVUH_S|A5tt?U_^a_Ns;B4TB=Af^9p8dCqd(j`$S;e?%~Bztces^rKbS4S zUmh}M{BPB&axl2pm%`b~E^N|Gy1CXW2ZP;3a^;x9Puq-(MTK=nDX#1w3cu`XfcP6J z%YBWHS%vgvMACuwOxoJ5EWd7~G+Gjy??TT~zkU;z=~)d_0t(0b4RCDIB9BSDB?N=?T0(6n{@KtxGQ`lFrsUp7uc*8L{JXDz|q z=={6^#lqi{iz5mYn7*=@8=6?fY=W?|uH_v(VS{8JVRKX6ENJF_r!DS|L^1429zds; z$5!}Izp`0uZPJn5M&W5TEZH|<dD zHTO@%u^`m(@%F@Ol3pyo^ca1B4Y7<)wt0|CzQiShF-$XdJj#;^-%{V%G&+fY4WemV ztIXJUT+2(_q5WUXd9cK-LwjnQWy2#p=$A5HP0kYfG zs*(oW!q^sUQjh?Lrj8q_U4lB-Gfp`3gH1dfR|&}+L*h(yKs6IVgacO2sIi9%-(=~kH zMuo{a6tzKA*ZL0f5JD!u*jL0~b-vUKJm;J!BCzmA85<<8sm^BQ&is^BTiIDK?!ebH zl!#}RO7=v>aCEn49cl5Ix;j`OqU|uJ^r{h;^4#WW@6|qPy0ubt@pqCIq2$xRtrK?s z&dXcEf7F`mD-K*6LvZ+8Kok<3R38Z`Z->tmvR#BrqoqvJW-0HUZ_p>{k@wK?NjzF!;U;Sql*ct0ReCT)u?{J3Ewt&3C(n>`LMICgVj!vHxD59=gdf~Wx?4xM5;xMp#fqE%!ZjW$sjUz9xy zJP-S*nxobWw(KmBjGL2il9PsgoB++PB8Yop~Vl3{e@qsr4`pwQu0qwG(dMk_nx#Ol>7TyMo;aTYD_4&2au(Mi#E zFcb;yfhRtAcJf73VMSswF!RJb$a8b z1b%rBXaaGHyfw!C&l zL?rq-lW*eAW~L6fB0h(-eLfj%IpSB5uv!^A2=kT%ZgGt3_o{t4o@IY0f>h*B$vj}9 zoL|i}2WTG0TJmJ5$xuKf?kqv`U4&wu9bY6>LoBMH0O#6077DP~jSndsk^Ag&)<%h5 zp?O3TVrel^xP*Qa3!O`9(cxjbK`9kXbBFKE{EXbz;w2p3!Bl1H0FL71r7)Gm`U)sD zxyF^UZT0S25<&bB7d1ZSJK2QLA?qTg#olR`C&Gu`^G*haJ~|mv)tXn(2WpEqd}pr( zqj#^iqsL>b&0q#re=%TD0Y6;~b>)g9NJ&an$~^uc=D0!0CMtE0$0sgq)>i_tR7`-swIzpfOIw zg>eHg`ZL#vcg>u7I&p_d=O~CY&?wFwI!#;`dmL*cFrrS12$vzqmo2&!jsMvEk;I&G za2`sfQ1yoMfqMNVR*9&{l}q{U<)+J*%!Lx1fD4<=HeVG_HNmk$NXU;~SHKF^Eb0!g zy^efgPx3e;51(8Sg4A<1|D=VO8vQnE*k6E5hx6&jMf2_b-OI&n2}dX6O`Ajqu~;Uo zd;inGrZ1)g)hWG9Vqj&i^|xs4y*m>vNCHG98t;Zev zos1x)ih`YRE#j`FvR}CQC!`P=h~u%COOAUb#`|1m1$wfL^bRWTSgAM#?RQ1b#0z@n zCM_LuvE3_y=^|;W`gycX(ceo1<|~+dwB-iRWyTd+&Q!@ z3EO=_SDpN0V^+)3A2NK8q@;qs0K;&oSQCf&8NCj8DELBKwnQn6QUc7XJDNx29H#Ym z3%mqQJN>-c*Mq99#B-js)SMlrleCY^seQtJ7n3 z(gGkaKWx@3fxF#w`Y!&incT;n-}R=7y_PE%o#)Jhm!r_zQ#IuFmdupVCH_aGbnEsN zr%uC%53JwC+pKLJlBqvz?Dz;>6#b(*@Ok3v@rbAxv(PhbW`DHAPenmPcz05OoUWGR zPxNj?X06K+iuaX)g$mUA3x<7}UjH_@L4P4C`t*hh+E%b8uJ3MKVimjKty297vta$5 zS&T1`=&v*Y4y#(Uns@cr`a}tBq3a?~((3rw@!_s|_G7T(8FzkZE+|2`TOshJu@l=T zIXO}@mt+G)w?EvT7G119Y}4+*t4?5SIy#qu-1S~Rbf6MAuc{=%CenvB;C*9Xu^Hg z_UBAp$)k?#$d9ohggr6<2Z_K;M&Pv6g0sopF5f`82Fbn@gB#P)cB#|=R} zq;(2;VBl8LdcD2?6{{yXH*=1xfSj*Akuh1G8>VCLww8lf0M3c=KX)rydj{E|s>_iN z;2njS!B&_WPO~-EeJ5g$Uo-A-VHy078=j?etr~C8;Cl8@s+EKEUZN@cArrw(UYO7m zX1nLnhWHEZ1mtgb8FZ{NREqixm^L(_-R9?GHx0b%jOh#YP_-f?tSPV^qTba$jNiHO zfT&}Z;1XU436nI+xWa)~ATwZ{X*p^$Q@0TOT_yp#eJb0P$lOGiwhzCo*GI#qqcIw8 zDkE_$qrk)0?lKpmY2TC&q4US)lDM;)*xwF2_$cWdmGJEi=eiw?``<@>oaf{%BMW{J zV~MXyN%y;YTHZe@*x^a6b?D;VrUFeP21|$y&My~A?d`-Cnf{h}<+_GHivMgZ_r3C!v<^0#Jei+JBVsQkN!NQ%l&YwUQy)Kx z3p*uuwi|7Xuh7qhX`AfPN1{P6$@>Se+L~3vQM)1?%Mn!)9O)xY_#i8)zYkvzSj}8I)pr!N9WO|US=osEyUX_J z{VenNVl0_^a`Pi!UVX0JD4XQOYnijBhmdq#v?DXc$`OkBibk}e;ye}r3OZ`pY51$kedm5V;P4(lCW*^qh8MpaeQNybixA?My zdV*AcWt0_w8Q$6e!g_Ne$t1|*JYo$z3ge?5B@=OkiI^l24knU_&si%1$38Ev-^klh zsC!;_yS|?sHVioHgCr~Ej!NB$=G|4~MIePdGw;MPB_l1u9DN~gn`vkW7_3j`DgKt( z+<2ovVA&LdyhR#naL{16idCfErs?(ja~Pe!iL=0}K9VjPx;GYf{!}WR)@isGQ_X#c z;KtZ6G_KQlj)S**tn$puEHT^lzT#0$!`HqAQ2&H4p?#M&BD}e9K~4ruqId-Ja+#bA zdZbvS5at%H#Q59V87@w@Cf5tgYsPO(gy*}2*Y)`TrX@U5dGwYMZ!i?_rmWV1I?=Oz zY~QwraI_c3X6-SF7+5@uoKI*Pou=8qXH!V^-tq|?U!A6W+33UkxUSmCx#T0*IwUv;TIqqA0Hv%Sku80v7gJ8Z(eB4U;Ju z)jaIbs4j6g;6deA18P5(4IChm*bH;z-w<;3>4*I3WbAO~#pcM0S3_1UY!}6!=bN9# zSVK8An`ov`%QX%;;)2lqDj$OAUjsj~>$Ap%NxiCQdblY^*S^NTZYw^fMz_3O%iAC@ z|ERvzQZ_zdvL2{xkLMFx2eoa+%X27YL?PYO2<#O8$w| zZd+>I`nA99Ooaw+!rRE*cduhvG;H#FSqBUOb}M(wQxa?ectR|A zs^7m;95b2C9fnwqU`pSCrB}H`q&5czZD;Ym=XXism3G+vQKHjq%>p^ad4{>!fXrFM zgu9#^C8JAXll-RT{t!HdOQqjwPu(W25Akaa(P45t?h0`Q_Czp8BB^y}Ek6pnr2L0x z-w-rnR!^z4Tf}$gio}ryB@tpJ;-=dagvq}B3Jl}(Wo+LKL3swx=ED%99oNS-br8=y ztLnaY+F!c%i~1p;{#&}(w{w&f=W>>~9+ibl zVSgosxJ~4Rd`_&$pS{h&C-8!aw>im;7nU;O%a5#wYwuOKPZ-d)XP$JY%uj|NIPJ?5 zU^#)B^?yi_?dmls6{^sr7Up-dWAVIn*lk2Io~$Puumb9T;;$fD^mn?0LaGy#vTIc~ z`UEXdQBO%+?mqetN|7=u?BV?(9)gX&&+(1x)RcU$3G086X7l*LTQV{m*2SfaZo|s- zq;KEDrqv?peai4KxY{`P*wi;mYs#pWN~ugIn{;!@ynoi)!ba$v@>I3H@v*Y*h@BPH z)|q>m&sS_XxtOXKDZJzqllJ)mvcuAZQ}`)Cv<}<+XvNw2XNU!>u0Ri(PoR-2T6t!r z>9EA+JX9-E5veWqu{GI`{6N)8_DO;#e8Nk*R90m90otaz@K(NxSgMY-*9UaVn?LMuZfDZ6=LprX!04#rayJE;)h>*m-{0w>bNH zx8&RPKZ_bR_L-Ib!aYB4G*kFJVo{gNAtup80x&<^wWZ!3i_bCqt-xkO zHa+jdy9i>oO?MWOYi91&;3x{tSc0zf z6eTLcbz00y*yjZyzOnqMnYeac)XRX8AM1~~dF0s|LxU*7uQN7^c6@`MnU&syKp^;4 zjagJmteG0)ob!)E4?^sc8kxUBlZfuqn9)vssrD@DWI`prte4lt88vX@4CLFovT83i z9yV(+TN)s9o8yVRt0J;r0i#5lfr}N!x0A&V7=k8Ftj@hrYqhmX6lGA`L(?GTFivBYQG)}bj`@EUztMw`&I8VGT-TX%#kTY-AVZ* zhiZOeYNlbEEm5VXQ8aHcL_9m!DNAZuZa3Eq1;WM`lsXPCm|sZFtui}dk=@oJ7S?Vr zq=~|Yqk*?P*KE(jZIku+%QRu@WrZIws?SBcSmqw#x33q7bUJa9cYg%N*c>{6mo538 zq-M3gV6Gt$Mye!aECR;9M(R;HRCNrZ(Ta6BUaA+l{mHynkbq-3wk>cgwHn{TgYO}qQC=E8f`&s64>Ax|@L+;MSg_GfzhGT*Ye zDIbnjFKUqU5}=oyT@9{P{ZXv%MDm(1=1eT{#eScc!S_vP;I}VzSTRRu$2l*o8fI(G zD=!eXSf`FB@elu1hSbkikNQ%AT>duQCDs5I^B+{z86D*OS$@hd0yF4#_3C?eqfERO$p8c{}%j)taiBlE+;+!V4;VPAhE`2OamF-3V$Zs@VCi+@3qkurc+ zC}74d-bj%@P-p~ISCtf3sY|nay$MA@R8gDlMmIBy^Q$3IorjcXAMQ#QuKf%2_@I=V zK^Oh*xSFhA5fSiFCd|O`?r--?OFvQ4ki%KXFSPCmK>LFho=CDU=Ka?w7T!m3_1B+` zC8S=NppY+|IOzPwD6-nQSz%Idky&xi+HlpgQipoYYRloF;~Zv(t%+?%k2j3Q!^Q>A zr^1PmO~*jc>pWNM@^dnwEpXem^@@4uCkm5wEIz}$NZ69-+?Q(BAUJX(+Z>@9B4Nci zIHveNJd%fb|Kwo^W4Bhs9&Yi1yc=Hh+3=k@=yTAvHPF80UyA0%0_c>e!kf=q5j`A~ zG}`P%EHCM})7dR`+1z=U_@SqxNZa((e}TUn+HF|>!>+;FsquMXP;KC_DY zL>DAmk(B^dS=%aWvfiSkGZ@1F(}y5IOo39-+MCa>FLxR1#qpLj&+hfMWj^SF`*=A5 zi$~*n*Xw9gTftx^F5_u(hN7qcU|eaR+q!I+^XUa^c;i!K$_2MQWuXBC(7L54nV3>x zba-?W6=?c!Ipe8vWsDlkfFg8vDL5lrR#eXCBlbh_Opf3Bj1?8>Vi{2C6qE%LvaF}! zm?1@p)7nuacx=Op$}j-27=}@}5R_pvUC5kI!SUa<+yJ2820fjr%|Rs;eb~9d<@GgK zoNQlVA~lJ57A<^E1$HWAto&SN6D$Z)VHKZAgjN309Nq%%Qz?Rcj`r{_m6FP`kXD0U zRtZJLxh$HBpp67q2cImFvqqjFH7ntk$!@KHm_=E-wB#PKHb}-BPW3cd(wHlRf&>vC z>1C!88{;7=0=H$9c6=Bmrd_aBq|$H-AbEh?p$-lr^{V%U+l@lef`cSWj*G;zv4wtb zSdWEg3G6KxglXSG5J3N9UDWS93@bGK?-A~F%yo1E+9+6aluJ)!83E1*D+z7NAvXD( z;DT}*3I(~X5%H28n!jGT`$_I#UkggNC{a?S*>DtT3gUUh$~sFTp#U;|B^h^RdLZ8f z!3W|lVBQe-+t!lA2hkYMUM?yby+brT_K!SGmP_GP%D^9*8DqHuF85TcC)WkoND+2r zXp71SQd3qnUq1X2rQ&T%K@<_8&@oNF$RKC9>P@q4`ps7&?*b9NYy5v2>3=3p-ryuQl;Pl=3 z$|C9~`vxlm_(T%&V{e9w-!LWQvx(Gu#rqSuuGy>K1@4EPrR>_0D}N}Ee+&#$h=VEERR)h$g2m zSI(cQDmSr)iQF?gnNq!|U3@C%=8rlN>u3GpKp6P?E)g`hcUK5;!m>_-I#3dry4jW_3g-)M|4FZ z3wpU~P#X!qQ`OJ&eVagLU1v_*m`W;NsgA|@L;1;9-tLu%?WsQ=Z&sXQ-@7xe`4<|O zOB*(m148h*mpwrQ({zkug8VcJrocKxmzx1>EnG4+wDYKmq5zwECf> zjsB2!-1w5ZCqSNJ!cjf5Byu<3P_d%hCIw9Ii8Gze7;hV;)}i}7Pp`18(F`(SqX5N*+b7$^BK}J97G_wa({WJWpIf8h1Y`e2GL0%md5SBM7F{3>nRmQ*(j{dgjC5hlO=OgKKdx0H?u zQhW_P`%ELb%W`$9pccO2T-NkIz(UsG17>gj44~148s=3+)_+4Q%Yj2+fc?KQ^8X}> u{}jppH~$Z){C_w9-!jO5UEu$}nd*Nf5@k7Pz<)mw|7o}Xs07o0xBme$gFCqZ literal 0 HcmV?d00001 diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/CollectionDepositManagerDSpace.java b/dspace-swordv2/src/main/java/org/dspace/sword2/CollectionDepositManagerDSpace.java index 7f77c00f0d9b..afc35cfaa11c 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/CollectionDepositManagerDSpace.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/CollectionDepositManagerDSpace.java @@ -151,6 +151,9 @@ public DepositReceipt createNew(String collectionUri, Deposit deposit, sc.commit(); return receipt; + } catch (SwordAuthException e) { + log.error("caught exception:", e); + throw e; } catch (DSpaceSwordException e) { log.error("caught exception:", e); throw new SwordServerException( diff --git a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordMETSContentIngester.java b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordMETSContentIngester.java index a77caa655b5d..45f0f315aa28 100644 --- a/dspace-swordv2/src/main/java/org/dspace/sword2/SwordMETSContentIngester.java +++ b/dspace-swordv2/src/main/java/org/dspace/sword2/SwordMETSContentIngester.java @@ -10,10 +10,12 @@ import java.io.File; import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; +import org.dspace.content.crosswalk.CrosswalkException; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.packager.PackageIngester; import org.dspace.content.packager.PackageParameters; @@ -28,6 +30,7 @@ import org.swordapp.server.SwordAuthException; import org.swordapp.server.SwordError; import org.swordapp.server.SwordServerException; +import org.swordapp.server.UriRegistry; public class SwordMETSContentIngester extends AbstractSwordContentIngester { /** @@ -181,6 +184,12 @@ public DepositResult ingestToCollection(Context context, Deposit deposit, } catch (RuntimeException re) { log.error("caught exception: ", re); throw re; + } catch (AuthorizeException e) { + log.error("caught exception: ", e); + throw new SwordAuthException(e); + } catch (CrosswalkException e) { + log.error("caught exception: ", e); + throw new SwordError(UriRegistry.ERROR_BAD_REQUEST, e.getMessage(), e); } catch (Exception e) { log.error("caught exception: ", e); throw new DSpaceSwordException(e); From 9c6724ee990524bf343cb9d220927437bf723013 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Sat, 7 Mar 2026 09:37:39 +0100 Subject: [PATCH 635/701] Avoid exception during OAI update caused by uncached entities (cherry picked from commit c3ac49dc64135ab48b34ef3766fcb5981cd94fbb) --- .../main/java/org/dspace/xoai/app/XOAI.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java index c8babc036e3c..596e3a6bac23 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java @@ -190,9 +190,9 @@ private int index(Date last) throws DSpaceSolrIndexerException, IOException { .findInArchiveOrWithdrawnDiscoverableModifiedSince(context, last); Iterator nonDiscoverableChangedItems = itemService .findInArchiveOrWithdrawnNonDiscoverableModifiedSince(context, last); + int total = this.index(discoverableChangedItems, true) + this.index(nonDiscoverableChangedItems, true); Iterator possiblyChangedItems = getItemsWithPossibleChangesBefore(last); - return this.index(discoverableChangedItems) + this.index(nonDiscoverableChangedItems) - + this.index(possiblyChangedItems); + return total + this.index(possiblyChangedItems, false); } catch (SQLException ex) { throw new DSpaceSolrIndexerException(ex.getMessage(), ex); } @@ -262,7 +262,7 @@ private int indexAll() throws DSpaceSolrIndexerException { null); Iterator nonDiscoverableItems = itemService .findInArchiveOrWithdrawnNonDiscoverableModifiedSince(context, null); - return this.index(discoverableItems) + this.index(nonDiscoverableItems); + return this.index(discoverableItems, true) + this.index(nonDiscoverableItems, true); } catch (SQLException ex) { throw new DSpaceSolrIndexerException(ex.getMessage(), ex); } @@ -305,7 +305,7 @@ private boolean checkIfVisibleInOAI(Item item) throws IOException { } } - private int index(Iterator iterator) throws DSpaceSolrIndexerException { + private int index(Iterator iterator, boolean uncacheEntities) throws DSpaceSolrIndexerException { try { int i = 0; int batchSize = configurationService.getIntProperty("oai.import.batch.size", 1000); @@ -334,10 +334,12 @@ private int index(Iterator iterator) throws DSpaceSolrIndexerException { server.add(list); server.commit(); list.clear(); - try { - context.uncacheEntities(); - } catch (SQLException ex) { - log.error("Error uncaching entities", ex); + if (uncacheEntities) { + try { + context.uncacheEntities(); + } catch (SQLException ex) { + log.error("Error uncaching entities", ex); + } } } } From a3e4b2468b809b1a4b1b0c2ed9e51c4bb1b265a6 Mon Sep 17 00:00:00 2001 From: Quasipoint <224759579+quasipoint@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:18:34 +0100 Subject: [PATCH 636/701] Serve OAI-PMH app as pre-transformed HTML according to Accept header Fix #11648 This will serve pre-transformed HTML using the existing XSLT stylesheet if the client sends an HTTP Accept header which asks for HTML with a higher priority than any XML-based format (either application/xml or text/xml). The client *must* explicitly ask for text/html or it will get the XML as at present. This should be tested with as many existing OAI-PMH harvesters as possible to ensure that none of them request text/html and thus will get HTML instead of XML. (cherry picked from commit 4b52cd0e616e94f6a037260d764a44c76af177ab) --- .../controller/DSpaceOAIDataProvider.java | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java b/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java index 3d826152c6ba..8e0f342f4ed3 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java @@ -11,6 +11,8 @@ import static java.util.Arrays.asList; import static org.apache.logging.log4j.LogManager.getLogger; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Enumeration; @@ -18,6 +20,13 @@ import java.util.List; import java.util.Map; import javax.xml.stream.XMLStreamException; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; import com.lyncode.xoai.dataprovider.OAIDataProvider; import com.lyncode.xoai.dataprovider.OAIRequestParameters; @@ -25,6 +34,7 @@ import com.lyncode.xoai.dataprovider.exceptions.InvalidContextException; import com.lyncode.xoai.dataprovider.exceptions.OAIException; import com.lyncode.xoai.dataprovider.exceptions.WritingXmlException; +import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -41,6 +51,10 @@ import org.dspace.xoai.services.impl.xoai.DSpaceResumptionTokenFormatter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; @@ -57,6 +71,8 @@ public class DSpaceOAIDataProvider { private static final Logger log = getLogger(DSpaceOAIDataProvider.class); + private Transformer htmlTransformer = null; + @Autowired XOAICacheService cacheService; @Autowired @@ -72,6 +88,25 @@ public class DSpaceOAIDataProvider { private DSpaceResumptionTokenFormatter resumptionTokenFormat = new DSpaceResumptionTokenFormatter(); + @PostConstruct + public void setUpHTMLTransformer() { + try { + XOAIManager manager = xoaiManagerResolver.getManager(); + if (manager.hasStyleSheet()) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + String styleSheetPath = manager.getStyleSheet(); + Resource styleSheetResource = resourceLoader.getResource("classpath:" + styleSheetPath); + Source htmlTransformSource + = new StreamSource(new ByteArrayInputStream(styleSheetResource.getContentAsByteArray())); + TransformerFactory transformerFactory + = TransformerFactory.newInstance(); + htmlTransformer = transformerFactory.newTransformer(htmlTransformSource); + } + } catch (Exception e) { + log.warn("Could not set up HTML transformer for OAI-PMH app: " + e.toString()); + } + } + @RequestMapping("") public void index(HttpServletResponse response, HttpServletRequest request) throws IOException { response.sendRedirect(request.getRequestURI() + "/"); @@ -92,7 +127,8 @@ public String indexAction(HttpServletResponse response, Model model) throws Serv @RequestMapping("/{context}") public String contextAction(Model model, HttpServletRequest request, HttpServletResponse response, - @PathVariable("context") String xoaiContext) throws IOException, ServletException { + @PathVariable("context") String xoaiContext) + throws IOException, ServletException, TransformerException { Context context = null; try { request.setCharacterEncoding("UTF-8"); @@ -109,7 +145,26 @@ public String contextAction(Model model, HttpServletRequest request, HttpServlet OutputStream out = response.getOutputStream(); OAIRequestParameters parameters = new OAIRequestParameters(buildParametersMap(request)); - response.setContentType("text/xml"); + boolean shouldServeAsHTML = false; + List acceptMediaTypes = MediaType.parseMediaTypes(request.getHeader("Accept")); + if (htmlTransformer != null) { + response.addHeader("Vary", "Accept"); + for (MediaType acceptMediaType : acceptMediaTypes) { + if (acceptMediaType.includes(MediaType.TEXT_XML) || + acceptMediaType.includes(MediaType.APPLICATION_XML)) { + break; + } else if (acceptMediaType.includes(MediaType.TEXT_HTML)) { + shouldServeAsHTML = true; + } + } + } + + if (shouldServeAsHTML) { + response.setContentType("text/html"); + out = new ByteArrayOutputStream(); + } else { + response.setContentType("text/xml"); + } response.setCharacterEncoding("UTF-8"); String identification = xoaiContext + parameters.requestID(); @@ -124,6 +179,13 @@ public String contextAction(Model model, HttpServletRequest request, HttpServlet dataProvider.handle(parameters, out); } + if (shouldServeAsHTML) { + OutputStream responseOut = response.getOutputStream(); + Source source = new StreamSource(new ByteArrayInputStream(((ByteArrayOutputStream) out).toByteArray())); + Result result = new StreamResult(responseOut); + htmlTransformer.transform(source, result); + out = responseOut; + } out.flush(); out.close(); From c1424b50357d71e3ac3eefccfab66705eb4962c6 Mon Sep 17 00:00:00 2001 From: Quasipoint <224759579+quasipoint@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:38:17 +0100 Subject: [PATCH 637/701] Make the server-side rendered OAI HTML interface configurable on/off (cherry picked from commit d9359c6520098bbc0c5cdf83f38926e087f7bceb) --- .../controller/DSpaceOAIDataProvider.java | 26 ++++++++++++------- dspace/config/modules/oai.cfg | 8 ++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java b/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java index 8e0f342f4ed3..e070725de7be 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/controller/DSpaceOAIDataProvider.java @@ -40,6 +40,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.logging.log4j.Logger; import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; import org.dspace.xoai.services.api.cache.XOAICacheService; import org.dspace.xoai.services.api.config.XOAIManagerResolver; import org.dspace.xoai.services.api.config.XOAIManagerResolverException; @@ -71,7 +72,8 @@ public class DSpaceOAIDataProvider { private static final Logger log = getLogger(DSpaceOAIDataProvider.class); - private Transformer htmlTransformer = null; + private TransformerFactory htmlTransformerFactory = null; + private byte[] htmlTransformerSource = null; @Autowired XOAICacheService cacheService; @@ -85,24 +87,28 @@ public class DSpaceOAIDataProvider { IdentifyResolver identifyResolver; @Autowired SetRepositoryResolver setRepositoryResolver; + @Autowired + ConfigurationService configurationService; private DSpaceResumptionTokenFormatter resumptionTokenFormat = new DSpaceResumptionTokenFormatter(); @PostConstruct - public void setUpHTMLTransformer() { + public void setUpHTMLTransformerFactory() { try { XOAIManager manager = xoaiManagerResolver.getManager(); - if (manager.hasStyleSheet()) { + if (configurationService.getBooleanProperty("oai.html", true) && manager.hasStyleSheet()) { ResourceLoader resourceLoader = new DefaultResourceLoader(); String styleSheetPath = manager.getStyleSheet(); Resource styleSheetResource = resourceLoader.getResource("classpath:" + styleSheetPath); - Source htmlTransformSource - = new StreamSource(new ByteArrayInputStream(styleSheetResource.getContentAsByteArray())); - TransformerFactory transformerFactory - = TransformerFactory.newInstance(); - htmlTransformer = transformerFactory.newTransformer(htmlTransformSource); + htmlTransformerSource = styleSheetResource.getContentAsByteArray(); + htmlTransformerFactory = TransformerFactory.newInstance(); + // run for potential exceptions only as a sanity check on the XSLT: + htmlTransformerFactory.newTransformer( + new StreamSource(new ByteArrayInputStream(htmlTransformerSource))); } } catch (Exception e) { + htmlTransformerFactory = null; + htmlTransformerSource = null; log.warn("Could not set up HTML transformer for OAI-PMH app: " + e.toString()); } } @@ -147,7 +153,7 @@ public String contextAction(Model model, HttpServletRequest request, HttpServlet boolean shouldServeAsHTML = false; List acceptMediaTypes = MediaType.parseMediaTypes(request.getHeader("Accept")); - if (htmlTransformer != null) { + if (htmlTransformerFactory != null) { response.addHeader("Vary", "Accept"); for (MediaType acceptMediaType : acceptMediaTypes) { if (acceptMediaType.includes(MediaType.TEXT_XML) || @@ -183,6 +189,8 @@ public String contextAction(Model model, HttpServletRequest request, HttpServlet OutputStream responseOut = response.getOutputStream(); Source source = new StreamSource(new ByteArrayInputStream(((ByteArrayOutputStream) out).toByteArray())); Result result = new StreamResult(responseOut); + Transformer htmlTransformer = htmlTransformerFactory.newTransformer( + new StreamSource(new ByteArrayInputStream(htmlTransformerSource))); htmlTransformer.transform(source, result); out = responseOut; } diff --git a/dspace/config/modules/oai.cfg b/dspace/config/modules/oai.cfg index 87603b1d5da5..376fd6aec190 100644 --- a/dspace/config/modules/oai.cfg +++ b/dspace/config/modules/oai.cfg @@ -16,6 +16,14 @@ oai.enabled = true # (Requires reboot of servlet container, e.g. Tomcat, to reload) oai.path = oai +# Whether or not to enable the OAI HTML interface, useful for +# debugging and learning about how OAI-PMH works +# For the HTML interface to work, the stylesheet must also be +# configured in config/crosswalks/oai/xoai.xml +# (by default dspace-oai/src/main/resources/static/style.xsl) +# Support for the OAI HTML interface is turned on by default. +#oai.html = true + # Storage: solr | database (solr is recommended) oai.storage=solr From 7230be47332d234dc25e74ee6cf423b6a7530eb2 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 24 Apr 2026 16:05:51 -0500 Subject: [PATCH 638/701] Bug Fix: Add missing Entity-related metadata fields required by virtual-metadata.xml --- dspace/config/dspace.cfg | 1 + dspace/config/registries/journal-types.xml | 37 +++++++++++++++++++ .../config/registries/schema-person-types.xml | 9 +++++ 3 files changed, 47 insertions(+) create mode 100644 dspace/config/registries/journal-types.xml diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 09ce15137cf0..d25a935260dc 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -954,6 +954,7 @@ registry.metadata.load = dspace-types.xml registry.metadata.load = iiif-types.xml registry.metadata.load = datacite-types.xml registry.metadata.load = coar-types.xml +registry.metadata.load = journal-types.xml #---------------------------------------------------------------# #-----------------UI-Related CONFIGURATIONS---------------------# diff --git a/dspace/config/registries/journal-types.xml b/dspace/config/registries/journal-types.xml new file mode 100644 index 000000000000..c68bee1ce6c1 --- /dev/null +++ b/dspace/config/registries/journal-types.xml @@ -0,0 +1,37 @@ + + + + + + DSpace Journal Types + + + + + + journal + http://dspace.org/journal + + + + journal + title + + The title of the Journal related to this object + + + + + journalvolume + http://dspace.org/journalvolume + + + + journalvolume + identifier + name + The identifier name for the Journal Volume related to this object + + + diff --git a/dspace/config/registries/schema-person-types.xml b/dspace/config/registries/schema-person-types.xml index 0a40060e5101..55f1acbbb74d 100644 --- a/dspace/config/registries/schema-person-types.xml +++ b/dspace/config/registries/schema-person-types.xml @@ -156,4 +156,13 @@ Full name variant + + + person + contributor + other + + + \ No newline at end of file From d411b38e434f8f613c12f76f37469798c8a474d1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 24 Apr 2026 16:07:16 -0500 Subject: [PATCH 639/701] Bug Fix: RoleDisseminator must add EPerson email to "Name" attribute in METS because that's what RoleIngester.ingestDocument() expects to be in the "Name" --- .../java/org/dspace/content/packager/RoleDisseminator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java index 15e9f1b14494..48901b5ce5c4 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleDisseminator.java @@ -310,8 +310,8 @@ protected void writeGroup(Context context, DSpaceObject relatedObject, Group gro for (EPerson member : group.getMembers()) { writer.writeEmptyElement(MEMBER); writer.writeAttribute(ID, String.valueOf(member.getID())); - if (null != member.getName()) { - writer.writeAttribute(NAME, member.getName()); + if (null != member.getEmail()) { + writer.writeAttribute(NAME, member.getEmail()); } } writer.writeEndElement(); From aa9e0587279b9df8eb136838d47114a0ff6b10c8 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 24 Apr 2026 16:13:53 -0500 Subject: [PATCH 640/701] Bug Fix: Improve handling of missing or empty Bitstreams. While this shouldn't happen much in production, it may happen in dev/test environments. Ensure entire export and import doesn't fail when a single empty/missing Bitstream is encountered. Instead of an exception, log a warning. --- .../LicenseStreamDisseminationCrosswalk.java | 8 +++- .../packager/AbstractMETSDisseminator.java | 43 +++++++++++++++---- .../packager/AbstractMETSIngester.java | 20 ++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/LicenseStreamDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/LicenseStreamDisseminationCrosswalk.java index 46858747870d..b1854bfd85ae 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/LicenseStreamDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/LicenseStreamDisseminationCrosswalk.java @@ -8,6 +8,7 @@ package org.dspace.content.crosswalk; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.sql.SQLException; @@ -56,7 +57,12 @@ public void disseminate(Context context, DSpaceObject dso, OutputStream out) Bitstream licenseBs = PackageUtils.findDepositLicense(context, (Item) dso); if (licenseBs != null) { - Utils.copy(bitstreamService.retrieve(context, licenseBs), out); + try (final InputStream bitInputStream = bitstreamService.retrieve(context, licenseBs)) { + Utils.copy(bitInputStream, out); + } catch (Exception e) { + log.warn("Could not retrieve license file for Item with UUID={}. " + + "Leaving it out of generated package. Error{}", dso.getID(), e.getMessage()); + } } } } diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java index fd50ec8023e2..2161a222e1f2 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSDisseminator.java @@ -456,17 +456,37 @@ protected void addBitstreamsToZip(Context context, DSpaceObject dso, // contents are unchanged ze.setTime(DEFAULT_MODIFIED_DATE); } - ze.setSize(auth ? bitstream.getSizeBytes() : 0); - zip.putNextEntry(ze); + + long bitstreamSize = 0; + // If user is authorized to read this bitstream, attempt to retrieve it. if (auth) { - InputStream input = bitstreamService.retrieve(context, bitstream); - Utils.copy(input, zip); - input.close(); + try (final InputStream bitstreamInput = bitstreamService.retrieve(context, bitstream)) { + // Save bitstream size into Zip entry & put entry in Zip file. + bitstreamSize = bitstream.getSizeBytes(); + ze.setSize(bitstreamSize); + zip.putNextEntry(ze); + + // Copy bitstream contents to Zip file + Utils.copy(bitstreamInput, zip); + } catch (Exception e) { + log.warn("Adding zero-length file for Bitstream, uuid={}." + + " Bitstream is unable to be retrieved from assetstore." + + " Error={}", bitstream.getID(), e.getMessage()); + } } else { - log.warn("Adding zero-length file for Bitstream, uuid=" - + String.valueOf(bitstream.getID()) - + ", not authorized for READ."); + log.warn("Adding zero-length file for Bitstream, uuid={}" + + ", not authorized for READ.", bitstream.getID()); + } + + // If bitstreamSize is still zero, that means either we didn't have READ privileges + // or the bitstream could not be retrieved from storage. Either way, write a zero-length + // file into our Zip entry in place of the bitstream. + if (bitstreamSize == 0) { + ze.setSize(0); + zip.putNextEntry(ze); } + + // Close our zip entry zip.closeEntry(); } else if (unauth != null && unauth.equalsIgnoreCase("skip")) { log.warn("Skipping Bitstream, uuid=" + String @@ -629,6 +649,13 @@ protected MdSec makeMdSec(Context context, DSpaceObject dso, Class mdSecClass, ByteArrayOutputStream disseminateOutput = new ByteArrayOutputStream(); sxwalk.disseminate(context, dso, disseminateOutput); disseminateOutput.close(); + + // If our disseminated output has zero size, exit immediately (i.e. return a null mdSec). + // Likely, the outputstream failed to be created, so we cannot include it in this package. + if (disseminateOutput.size() == 0) { + return null; + } + // Convert output to an inputstream, so we can write to manifest or Zip file ByteArrayInputStream crosswalkedStream = new ByteArrayInputStream( disseminateOutput.toByteArray()); diff --git a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java index 68383f1d9e74..122f13992087 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/AbstractMETSIngester.java @@ -19,6 +19,7 @@ import java.util.zip.ZipFile; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.input.NullInputStream; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; @@ -748,6 +749,14 @@ protected void addBitstreams(Context context, Item item, // externally, if it is an externally referenced file) InputStream fileStream = getFileInputStream(pkgFile, params, path); + // Before proceeding we must ensure we have a non-empty input stream + // NOTE: If getFileInputStream encounters a zero-sized file, then it returns NullInputStream + if (fileStream == null || fileStream instanceof NullInputStream) { + log.warn("Empty InputStream encountered for Bitstream with ID={} in zip file={}. " + + "Skipping adding this empty bitstream to Item={}", mfileID, pkgFile, item.getID()); + continue; + } + // retrieve bundle name from manifest String bundleName = METSManifest.getBundleName(mfile); @@ -1324,7 +1333,7 @@ public String getObjectHandle(METSManifest manifest) * zip) * @param params Parameters passed to METSIngester * @param path the File path (either path in Zip package or a URL) - * @return the InputStream for the file + * @return the InputStream for the file, or NullInputStream if a zero-sized entry is encountered * @throws MetadataValidationException if validation error * @throws IOException if IO error */ @@ -1357,12 +1366,13 @@ protected static InputStream getFileInputStream(File pkgFile, // Retrieve the manifest file entry by name ZipEntry manifestEntry = zipPackage.getEntry(path); - // Get inputStream associated with this file - if (manifestEntry != null) { + if (manifestEntry.getSize() > 0) { + // Get inputStream associated with this file return zipPackage.getInputStream(manifestEntry); } else { - throw new MetadataValidationException("Manifest file references file '" - + path + "' not included in the zip."); + log.warn("Zero-sized file entry={} found in zip file={}. Returning empty InputStream.", + path, pkgFile); + return new NullInputStream(); } } } From a3610bab65f0db737f3e86e1c0f17dfa4cc46239 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 24 Apr 2026 16:15:34 -0500 Subject: [PATCH 641/701] Minor fix to exception message. Include the metadata schema name in the message text. --- .../content/crosswalk/CrosswalkMetadataValidator.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/CrosswalkMetadataValidator.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/CrosswalkMetadataValidator.java index b1be458a255e..1bcc1c9ba9fb 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/CrosswalkMetadataValidator.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/CrosswalkMetadataValidator.java @@ -107,9 +107,12 @@ public MetadataField checkMetadata(Context context, String schema, String elemen e.printStackTrace(); } } else if (!fieldChoice.equals("ignore")) { - throw new CrosswalkException( - "The '" + element + "." + qualifier + "' element has not been defined in this DSpace " + - "instance. "); + throw new CrosswalkException(String.format( + "The '%s.%s%s' element has not been defined in this DSpace instance.", + mdSchema.getName(), + element, + qualifier == null ? "" : ("." + qualifier) + )); } } } From c7f4937364460c66cb106c8665a7debb3fdbc195 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 12 May 2026 12:20:48 +0200 Subject: [PATCH 642/701] DSpace#12470 Force each JWT handler to fall back to documented defaults (cherry picked from commit 54b7b574416ac3bbf6791dfd2ee653f35939ea4e) --- .../app/rest/security/jwt/JWTTokenHandler.java | 13 ++++++++----- .../app/rest/security/jwt/LoginJWTTokenHandler.java | 11 +++++++++++ .../security/jwt/ShortLivedJWTTokenHandler.java | 10 ++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index 727267744fb1..f3161e453777 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -65,7 +65,7 @@ public abstract class JWTTokenHandler { private List jwtClaimProviders; @Autowired - private ConfigurationService configurationService; + protected ConfigurationService configurationService; @Autowired private EPersonClaimProvider ePersonClaimProvider; @@ -79,6 +79,13 @@ public abstract class JWTTokenHandler { private String generatedJwtKey; private String generatedEncryptionKey; + /** + * Get the default expiration period for this handler if not + * defined in configuration. + * @return default expiration period if not explicitly defined in configuration + */ + public abstract long getExpirationPeriod(); + /** * Get the configuration property key for the token secret. * @return the configuration property key @@ -220,10 +227,6 @@ public String getJwtKey() { return secret; } - public long getExpirationPeriod() { - return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), 1800000); - } - public boolean isEncryptionEnabled() { return configurationService.getBooleanProperty(getEncryptionEnabledConfigurationKey(), false); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java index 1fad84165809..46877b03c3ca 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/LoginJWTTokenHandler.java @@ -15,6 +15,17 @@ */ @Component public class LoginJWTTokenHandler extends JWTTokenHandler { + + /** + * Default expiration period for login tokens in milliseconds + */ + private static final long DEFAULT_EXPIRATION_PERIOD = 1800000; + + @Override + public long getExpirationPeriod() { + return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), DEFAULT_EXPIRATION_PERIOD); + } + @Override protected String getTokenSecretConfigurationKey() { return "jwt.login.token.secret"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java index ac7a73a796ef..94bf1f5b1d80 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandler.java @@ -28,6 +28,11 @@ @Component public class ShortLivedJWTTokenHandler extends JWTTokenHandler { + /** + * Default expiration period for short-lived tokens in milliseconds + */ + private static final long DEFAULT_EXPIRATION_PERIOD = 2000; + /** * Determine if current JWT is valid for the given EPerson object. * To be valid, current JWT *must* have been signed by the EPerson and not be expired. @@ -67,6 +72,11 @@ protected EPerson updateSessionSalt(final Context context, final Date previousLo return context.getCurrentUser(); } + @Override + public long getExpirationPeriod() { + return configurationService.getLongProperty(getTokenExpirationConfigurationKey(), DEFAULT_EXPIRATION_PERIOD); + } + @Override protected String getTokenSecretConfigurationKey() { return "jwt.shortLived.token.secret"; From f4ac54953b784a6905d4bfae466c83fc3d0f2266 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 12 May 2026 12:28:56 +0200 Subject: [PATCH 643/701] DSpace#12470 Doc and comment default expiration periods in authentication.cfg This further proves the forcing of each handler to set an expiration period is working and sets accurate expectations for implementers. (cherry picked from commit d4cb66f1c6f83c3998aefd4db76677e25f846294) --- dspace/config/modules/authentication.cfg | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/dspace/config/modules/authentication.cfg b/dspace/config/modules/authentication.cfg index 41c28df1d7c9..253035fe3e52 100644 --- a/dspace/config/modules/authentication.cfg +++ b/dspace/config/modules/authentication.cfg @@ -78,8 +78,9 @@ jwt.login.encryption.enabled = false # of some performance, this setting WILL ONLY BE used when encrypting the jwt. jwt.login.compression.enabled = true -# Expiration time of a token in milliseconds -jwt.login.token.expiration = 1800000 +# Expiration time of a login token in milliseconds +# Default: 1800000 (30 minutes) +#jwt.login.token.expiration = 1800000 #---------------------------------------------------------------# #---Stateless JWT Authentication for downloads of bitstreams----# @@ -103,5 +104,6 @@ jwt.shortLived.encryption.enabled = false # of some performance, this setting WILL ONLY BE used when encrypting the jwt. jwt.shortLived.compression.enabled = true -# Expiration time of a token in milliseconds -jwt.shortLived.token.expiration = 2000 +# Expiration time of a short-lived token in milliseconds +# Default: 2000 (2 seconds) +#jwt.shortLived.token.expiration = 2000 From e470e020368e73758aa652b66644f97a7e457a2c Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 12 May 2026 13:08:13 +0200 Subject: [PATCH 644/701] DSpace#12470 Remove unnecessary stubbing in short lived JWT test (cherry picked from commit ae45cecee456be71ae2e89220803e918d76ce682) --- .../app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java index d5a72ea9a5cd..3bc6d1376488 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/security/jwt/ShortLivedJWTTokenHandlerTest.java @@ -76,8 +76,7 @@ public void testJWTEncrypted() throws Exception { //temporary set a negative expiration time so the token is invalid immediately @Test public void testExpiredToken() throws Exception { - when(configurationService.getLongProperty("jwt.shortLived.token.expiration", 1800000)) - .thenReturn(-99999999L); + when(shortLivedJWTTokenHandler.getExpirationPeriod()).thenReturn(-99999999L); when(ePersonClaimProvider.getEPerson(any(Context.class), any(JWTClaimsSet.class))).thenReturn(ePerson); Date previous = new Date(new Date().getTime() - 10000000000L); String token = shortLivedJWTTokenHandler From cb8c3795824de2d8e4024034e60e19e6d7ed8639 Mon Sep 17 00:00:00 2001 From: Mark Patton Date: Mon, 17 Nov 2025 10:58:43 -0500 Subject: [PATCH 645/701] Handle creation of thumbnails for multi-image TIFF files (cherry picked from commit f28641d910e1133fbbdeb528491c70594f9190ea) --- .../org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java index 7543410a7968..28bfc72dc110 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/ImageMagickThumbnailFilter.java @@ -105,7 +105,7 @@ public File getThumbnailFile(File f, boolean verbose) ConvertCmd cmd = new ConvertCmd(); IMOperation op = new IMOperation(); op.autoOrient(); - op.addImage(f.getAbsolutePath()); + op.addImage(f.getAbsolutePath() + "[0]"); op.thumbnail(configurationService.getIntProperty("thumbnail.maxwidth", DEFAULT_WIDTH), configurationService.getIntProperty("thumbnail.maxheight", DEFAULT_HEIGHT)); op.addImage(f2.getAbsolutePath()); From 220df580f092c34bfee68afa4eeb2b936de6cad5 Mon Sep 17 00:00:00 2001 From: Istvan Vig Date: Mon, 9 Mar 2026 16:13:33 +0100 Subject: [PATCH 646/701] Controlled vocabulary for accented characters (cherry picked from commit e346526f3a5d0a2be4cb10c2f16cd07c2bd67f9b) --- .../dspace/content/authority/DSpaceControlledVocabulary.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 1bef8b389898..86a0c64d1703 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -61,8 +61,9 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); - protected static String xpathTemplate = "//node[contains(translate(@label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ'," + - "'abcdefghijklmnopqrstuvwxyz'),%s)]"; + protected static String xpathTemplate = "//node[contains(translate(@label," + + "'ABCDEFGHIJKLMNOPQRSTUVWXYZÁÀÂÄÃÅĄĂĆČÇĎĐÉÈÊËĘĚÍÌÎÏŁĽŃŇÑÓÒÔÖÕŐŔŘŚŠŞŤŢÚÙÛÜŮŰÝŸŹŽŻ'," + + "'abcdefghijklmnopqrstuvwxyzáàâäãåąăćčçďđéèêëęěíìîïłľńňñóòôöõőŕřśšşťţúùûüůűýÿźžż'),%s)]"; protected static String idTemplate = "//node[@id = %s]"; protected static String idTemplateQuoted = "//node[@id = '%s']"; protected static String labelTemplate = "//node[@label = %s]"; From 9837d71e695ccb498c1b148d117ce91b8b69f737 Mon Sep 17 00:00:00 2001 From: Istvan Vig Date: Wed, 11 Mar 2026 16:07:35 +0100 Subject: [PATCH 647/701] Fixes https://github.com/bme-omikk/repozitorium-config/issues/214 The fix is in the dspace/modules/additions folder. (cherry picked from commit c40868bdf9489fae34c3c4358876a0d1832daac7) --- .../authority/DSpaceControlledVocabulary.java | 5 +- .../authority/DSpaceControlledVocabulary.java | 496 ++++++++++++++++++ .../modules/server/src/main/webapp/.gitignore | 0 3 files changed, 498 insertions(+), 3 deletions(-) create mode 100644 dspace/modules/additions/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java delete mode 100644 dspace/modules/server/src/main/webapp/.gitignore diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 86a0c64d1703..1bef8b389898 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -61,9 +61,8 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); - protected static String xpathTemplate = "//node[contains(translate(@label," + - "'ABCDEFGHIJKLMNOPQRSTUVWXYZÁÀÂÄÃÅĄĂĆČÇĎĐÉÈÊËĘĚÍÌÎÏŁĽŃŇÑÓÒÔÖÕŐŔŘŚŠŞŤŢÚÙÛÜŮŰÝŸŹŽŻ'," + - "'abcdefghijklmnopqrstuvwxyzáàâäãåąăćčçďđéèêëęěíìîïłľńňñóòôöõőŕřśšşťţúùûüůűýÿźžż'),%s)]"; + protected static String xpathTemplate = "//node[contains(translate(@label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ'," + + "'abcdefghijklmnopqrstuvwxyz'),%s)]"; protected static String idTemplate = "//node[@id = %s]"; protected static String idTemplateQuoted = "//node[@id = '%s']"; protected static String labelTemplate = "//node[@label = %s]"; diff --git a/dspace/modules/additions/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace/modules/additions/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java new file mode 100644 index 000000000000..36ac58f4b66f --- /dev/null +++ b/dspace/modules/additions/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -0,0 +1,496 @@ +/** + * 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.content.authority; + +import java.io.File; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.core.SelfNamedPlugin; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +/** + * ChoiceAuthority source that reads the hierarchical vocabularies + * from {@code ${dspace.dir}/config/controlled-vocabularies/*.xml} and turns + * them into autocompleting authorities. + * + *

Configuration: This MUST be configured as a self-named plugin, e.g.: {@code + * plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = + * org.dspace.content.authority.DSpaceControlledVocabulary + * } + * + *

It AUTOMATICALLY configures a plugin instance for each XML file in the + * controlled vocabularies directory. The name of the plugin is the basename of + * the file; e.g., {@code ${dspace.dir}/config/controlled-vocabularies/nsi.xml} + * would generate a plugin called "nsi". + * + *

Each configured plugin comes with three configuration options: + *

    + *
  • {@code vocabulary.plugin._plugin_.hierarchy.store = + * # Store entire hierarchy along with selected value. Default: TRUE}
  • + *
  • {@code vocabulary.plugin._plugin_.hierarchy.suggest = + * # Display entire hierarchy in the suggestion list. Default: TRUE}
  • + *
  • {@code vocabulary.plugin._plugin_.delimiter = "" + * # Delimiter to use when building hierarchy strings. Default: "::"}
  • + *
+ * } + * + * @author Michael B. Klein + */ + +public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); + protected static String xpathTemplate = "//node[contains(translate(@label," + + "'ABCDEFGHIJKLMNOPQRSTUVWXYZÁÀÂÄÃÅĄĂĆČÇĎĐÉÈÊËĘĚÍÌÎÏŁĽŃŇÑÓÒÔÖÕŐŔŘŚŠŞŤŢÚÙÛÜŮŰÝŸŹŽŻ'," + + "'abcdefghijklmnopqrstuvwxyzáàâäãåąăćčçďđéèêëęěíìîïłľńňñóòôöõőŕřśšşťţúùûüůűýÿźžż'),%s)]"; + protected static String idTemplate = "//node[@id = %s]"; + protected static String idTemplateQuoted = "//node[@id = '%s']"; + protected static String labelTemplate = "//node[@label = %s]"; + protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy/parent::node"; + protected static String rootTemplate = "/node"; + protected static String idAttribute = "id"; + protected static String labelAttribute = "label"; + protected static String[] pluginNames = null; + protected String vocabularyName = null; + protected InputSource vocabulary = null; + protected Boolean suggestHierarchy = false; + protected Boolean storeHierarchy = true; + protected String hierarchyDelimiter = "::"; + protected Integer preloadLevel = 1; + protected String valueAttribute = labelAttribute; + protected String valueTemplate = labelTemplate; + + public DSpaceControlledVocabulary() { + super(); + } + + @Override + public boolean storeAuthorityInMetadata() { + // For backward compatibility controlled vocabularies don't store the node id in + // the metadatavalue + return false; + } + + public static String[] getPluginNames() { + if (pluginNames == null) { + initPluginNames(); + } + + return ArrayUtils.clone(pluginNames); + } + + private static synchronized void initPluginNames() { + if (pluginNames == null) { + class xmlFilter implements java.io.FilenameFilter { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".xml"); + } + } + String vocabulariesPath = DSpaceServicesFactory.getInstance().getConfigurationService() + .getProperty("dspace.dir") + + File.separator + "config" + + File.separator + "controlled-vocabularies"; + String[] xmlFiles = (new File(vocabulariesPath)).list(new xmlFilter()); + List names = new ArrayList<>(); + for (String filename : xmlFiles) { + names.add((new File(filename)).getName().replace(".xml", "")); + } + pluginNames = names.toArray(new String[names.size()]); + log.info("Got plugin names = " + Arrays.deepToString(pluginNames)); + } + } + + protected void init(String locale) { + if (vocabulary == null) { + ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService(); + + log.info("Initializing " + this.getClass().getName()); + vocabularyName = this.getPluginInstanceName(); + String vocabulariesPath = config.getProperty("dspace.dir") + File.separator + "config" + + File.separator + "controlled-vocabularies" + File.separator; + String configurationPrefix = "vocabulary.plugin." + vocabularyName; + storeHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.store", storeHierarchy); + boolean storeIDs = config.getBooleanProperty(configurationPrefix + ".storeIDs", false); + suggestHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.suggest", suggestHierarchy); + preloadLevel = config.getIntProperty(configurationPrefix + ".hierarchy.preloadLevel", preloadLevel); + String configuredDelimiter = config.getProperty(configurationPrefix + ".delimiter"); + if (configuredDelimiter != null) { + hierarchyDelimiter = configuredDelimiter.replaceAll("(^\"|\"$)", ""); + } + if (storeIDs) { + valueAttribute = idAttribute; + valueTemplate = idTemplate; + } + + String filename = vocabulariesPath + vocabularyName + ".xml"; + if (StringUtils.isNotEmpty(locale)) { + String localizedFilename = vocabulariesPath + vocabularyName + "_" + locale + ".xml"; + if (Paths.get(localizedFilename).toFile().exists()) { + filename = localizedFilename; + } + } + log.info("Loading " + filename); + vocabulary = new InputSource(filename); + } + } + + protected String buildString(Node node) { + if (node.getNodeType() == Node.DOCUMENT_NODE || ( + node.getParentNode() != null && + node.getParentNode().getNodeType() == Node.DOCUMENT_NODE)) { + return (""); + } else { + String parentValue = buildString(node.getParentNode()); + Node currentNodeValue = node.getAttributes().getNamedItem(valueAttribute); + if (currentNodeValue != null) { + String currentValue = currentNodeValue.getNodeValue(); + if (parentValue.equals("")) { + return currentValue; + } else { + return (parentValue + this.hierarchyDelimiter + currentValue); + } + } else { + return (parentValue); + } + } + } + + @Override + public Choices getMatches(String text, int start, int limit, String locale) { + init(locale); + log.debug("Getting matches for '" + text + "'"); + String[] textHierarchy = text.split(hierarchyDelimiter, -1); + StringBuilder xpathExpressionBuilder = new StringBuilder(); + for (int i = 0; i < textHierarchy.length; i++) { + xpathExpressionBuilder.append(String.format(xpathTemplate, "$var" + i)); + } + String xpathExpression = xpathExpressionBuilder.toString(); + XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setXPathVariableResolver(variableName -> { + String varName = variableName.getLocalPart(); + if (varName.startsWith("var")) { + int index = Integer.parseInt(varName.substring(3)); + return textHierarchy[index].toLowerCase(); + } + throw new IllegalArgumentException("Unexpected variable: " + varName); + }); + int total; + List choices; + try { + NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); + total = results.getLength(); + choices = getChoicesFromNodeList(results, start, limit); + } catch (XPathExpressionException e) { + log.warn(e.getMessage(), e); + return new Choices(true); + } + return new Choices(choices.toArray(new Choice[choices.size()]), start, total, Choices.CF_AMBIGUOUS, + total > start + limit); + } + + @Override + public Choices getBestMatch(String text, String locale) { + init(locale); + log.debug("Getting best matches for {}'", text); + String[] textHierarchy = text.split(hierarchyDelimiter, -1); + StringBuilder xpathExpressionBuilder = new StringBuilder(); + for (int i = 0; i < textHierarchy.length; i++) { + xpathExpressionBuilder.append(String.format(valueTemplate, "$var" + i)); + } + String xpathExpression = xpathExpressionBuilder.toString(); + XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setXPathVariableResolver(variableName -> { + String varName = variableName.getLocalPart(); + if (varName.startsWith("var")) { + int index = Integer.parseInt(varName.substring(3)); + return textHierarchy[index]; + } + throw new IllegalArgumentException("Unexpected variable: " + varName); + }); + List choices; + try { + NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); + choices = getChoicesFromNodeList(results, 0, 1); + } catch (XPathExpressionException e) { + log.warn(e.getMessage(), e); + return new Choices(true); + } + return new Choices(choices.toArray(new Choice[choices.size()]), 0, choices.size(), Choices.CF_AMBIGUOUS, false); + } + + @Override + public String getLabel(String key, String locale) { + return getNodeValue(key, locale, this.suggestHierarchy); + } + + @Override + public String getValue(String key, String locale) { + return getNodeValue(key, locale, this.storeHierarchy); + } + + @Override + public Choice getChoice(String authKey, String locale) { + Node node; + try { + node = getNode(authKey, locale); + } catch (XPathExpressionException e) { + return null; + } + return createChoiceFromNode(node); + } + + @Override + public boolean isHierarchical() { + init(null); + return true; + } + + @Override + public Choices getTopChoices(String authorityName, int start, int limit, String locale) { + init(locale); + String xpathExpression = rootTemplate; + return getChoicesByXpath(xpathExpression, start, limit); + } + + @Override + public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) { + init(locale); + String xpathExpression = String.format(idTemplateQuoted, parentId); + return getChoicesByXpath(xpathExpression, start, limit); + } + + @Override + public Choice getParentChoice(String authorityName, String childId, String locale) { + init(locale); + try { + String xpathExpression = String.format(idParentTemplate, childId); + Choice choice = createChoiceFromNode(getNodeFromXPath(xpathExpression)); + return choice; + } catch (XPathExpressionException e) { + log.error(e.getMessage(), e); + return null; + } + } + + @Override + public Integer getPreloadLevel() { + init(null); + return preloadLevel; + } + + private boolean isRootElement(Node node) { + return node != null && node.getOwnerDocument().getDocumentElement().equals(node); + } + + private Node getNode(String key, String locale) throws XPathExpressionException { + init(locale); + String xpathExpression = String.format(idTemplateQuoted, key); + Node node = getNodeFromXPath(xpathExpression); + return node; + } + + private Node getNodeFromXPath(String xpathExpression) throws XPathExpressionException { + XPath xpath = XPathFactory.newInstance().newXPath(); + Node node = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); + return node; + } + + private List getChoicesFromNodeList(NodeList results, int start, int limit) { + List choices = new ArrayList<>(); + for (int i = 0; i < results.getLength(); i++) { + if (i < start) { + continue; + } + if (choices.size() == limit) { + break; + } + Node node = results.item(i); + Choice choice = new Choice(getAuthority(node), getLabel(node), getValue(node), + isSelectable(node)); + choice.extras = addOtherInformation(getParent(node), getNote(node), getChildren(node), getAuthority(node)); + choices.add(choice); + } + return choices; + } + + private Map addOtherInformation(String parentCurr, String noteCurr, + List childrenCurr, String authorityCurr) { + Map extras = new HashMap<>(); + if (StringUtils.isNotBlank(parentCurr)) { + extras.put("parent", parentCurr); + } + if (StringUtils.isNotBlank(noteCurr)) { + extras.put("note", noteCurr); + } + if (childrenCurr.isEmpty()) { + extras.put("hasChildren", "false"); + } else { + extras.put("hasChildren", "true"); + } + extras.put("id", authorityCurr); + return extras; + } + + private String getNodeValue(String key, String locale, boolean useHierarchy) { + try { + Node node = getNode(key, locale); + if (Objects.isNull(node)) { + return null; + } + if (useHierarchy) { + return this.buildString(node); + } else { + return node.getAttributes().getNamedItem(valueAttribute).getNodeValue(); + } + } catch (XPathExpressionException e) { + return (""); + } + } + + private String getLabel(Node node) { + String hierarchy = this.buildString(node); + if (this.suggestHierarchy) { + return hierarchy; + } else { + return node.getAttributes().getNamedItem("label").getNodeValue(); + } + } + + private String getValue(Node node) { + String hierarchy = this.buildString(node); + if (this.storeHierarchy) { + return hierarchy; + } else { + return node.getAttributes().getNamedItem(valueAttribute).getNodeValue(); + } + } + + private String getNote(Node node) { + NodeList childNodes = node.getChildNodes(); + for (int ci = 0; ci < childNodes.getLength(); ci++) { + Node firstChild = childNodes.item(ci); + if (firstChild != null && "hasNote".equals(firstChild.getNodeName())) { + String nodeValue = firstChild.getTextContent(); + if (StringUtils.isNotBlank(nodeValue)) { + return nodeValue; + } + } + } + return null; + } + + private List getChildren(Node node) { + List children = new ArrayList<>(); + NodeList childNodes = node.getChildNodes(); + for (int ci = 0; ci < childNodes.getLength(); ci++) { + Node firstChild = childNodes.item(ci); + if (firstChild != null && "isComposedBy".equals(firstChild.getNodeName())) { + for (int cii = 0; cii < firstChild.getChildNodes().getLength(); cii++) { + Node childN = firstChild.getChildNodes().item(cii); + if (childN != null && "node".equals(childN.getNodeName())) { + Node childIdAttr = childN.getAttributes().getNamedItem("id"); + if (null != childIdAttr) { + children.add(childIdAttr.getNodeValue()); + } + } + } + break; + } + } + return children; + } + + private boolean isSelectable(Node node) { + Node selectableAttr = node.getAttributes().getNamedItem("selectable"); + if (null != selectableAttr) { + return Boolean.parseBoolean(selectableAttr.getNodeValue()); + } else { // Default is true + return true; + } + } + + private String getParent(Node node) { + Node parentN = node.getParentNode(); + if (parentN != null) { + parentN = parentN.getParentNode(); + if (parentN != null && !isRootElement(parentN)) { + return buildString(parentN); + } + } + return null; + } + + private String getAuthority(Node node) { + Node idAttr = node.getAttributes().getNamedItem("id"); + if (null != idAttr) { // 'id' is optional + return idAttr.getNodeValue(); + } else { + return null; + } + } + + private Choices getChoicesByXpath(String xpathExpression, int start, int limit) { + List choices = new ArrayList<>(); + XPath xpath = XPathFactory.newInstance().newXPath(); + try { + Node parentNode = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); + int count = 0; + if (parentNode != null) { + NodeList childNodes = (NodeList) xpath.evaluate(".//isComposedBy", parentNode, XPathConstants.NODE); + if (null != childNodes) { + for (int i = 0; i < childNodes.getLength(); i++) { + Node childNode = childNodes.item(i); + if (childNode != null && "node".equals(childNode.getNodeName())) { + if (count < start || choices.size() >= limit) { + count++; + continue; + } + count++; + choices.add(createChoiceFromNode(childNode)); + } + } + } + return new Choices(choices.toArray(new Choice[choices.size()]), start, count, + Choices.CF_AMBIGUOUS, false); + } + } catch (XPathExpressionException e) { + log.warn(e.getMessage(), e); + return new Choices(true); + } + return new Choices(false); + } + + private Choice createChoiceFromNode(Node node) { + if (node != null && !isRootElement(node)) { + Choice choice = new Choice(getAuthority(node), getLabel(node), getValue(node), + isSelectable(node)); + choice.extras = addOtherInformation(getParent(node), getNote(node),getChildren(node), getAuthority(node)); + return choice; + } + return null; + } + +} diff --git a/dspace/modules/server/src/main/webapp/.gitignore b/dspace/modules/server/src/main/webapp/.gitignore deleted file mode 100644 index e69de29bb2d1..000000000000 From b78edc03c85b4664f789131a5b276d961f76ad50 Mon Sep 17 00:00:00 2001 From: Istvan Vig Date: Tue, 24 Mar 2026 19:45:22 +0100 Subject: [PATCH 648/701] rebase on current main; refactor to use all unicode characters (cherry picked from commit 18427d009156f67251c692dab35d321ec337ed84) --- .../authority/DSpaceControlledVocabulary.java | 15 +- .../authority/DSpaceControlledVocabulary.java | 496 ------------------ .../modules/server/src/main/webapp/.gitignore | 0 3 files changed, 13 insertions(+), 498 deletions(-) delete mode 100644 dspace/modules/additions/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java create mode 100644 dspace/modules/server/src/main/webapp/.gitignore diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java index 1bef8b389898..3aea294b2ba1 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java @@ -61,8 +61,19 @@ public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); - protected static String xpathTemplate = "//node[contains(translate(@label,'ABCDEFGHIJKLMNOPQRSTUVWXYZ'," + - "'abcdefghijklmnopqrstuvwxyz'),%s)]"; + protected static final String xpathTemplate; + static { + StringBuilder upper = new StringBuilder(); + StringBuilder lower = new StringBuilder(); + for (int cp = 'A'; cp <= Character.MAX_CODE_POINT; cp++) { + if (Character.isLetter(cp) && Character.isUpperCase(cp)) { + int lcp = Character.toLowerCase(cp); + upper.appendCodePoint(cp); + lower.appendCodePoint(lcp); + } + } + xpathTemplate = "//node[contains(translate(@label,'" + upper + "','" + lower + "'),%s)]"; + } protected static String idTemplate = "//node[@id = %s]"; protected static String idTemplateQuoted = "//node[@id = '%s']"; protected static String labelTemplate = "//node[@label = %s]"; diff --git a/dspace/modules/additions/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java b/dspace/modules/additions/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java deleted file mode 100644 index 36ac58f4b66f..000000000000 --- a/dspace/modules/additions/src/main/java/org/dspace/content/authority/DSpaceControlledVocabulary.java +++ /dev/null @@ -1,496 +0,0 @@ -/** - * 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.content.authority; - -import java.io.File; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.Logger; -import org.dspace.core.SelfNamedPlugin; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -/** - * ChoiceAuthority source that reads the hierarchical vocabularies - * from {@code ${dspace.dir}/config/controlled-vocabularies/*.xml} and turns - * them into autocompleting authorities. - * - *

Configuration: This MUST be configured as a self-named plugin, e.g.: {@code - * plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = - * org.dspace.content.authority.DSpaceControlledVocabulary - * } - * - *

It AUTOMATICALLY configures a plugin instance for each XML file in the - * controlled vocabularies directory. The name of the plugin is the basename of - * the file; e.g., {@code ${dspace.dir}/config/controlled-vocabularies/nsi.xml} - * would generate a plugin called "nsi". - * - *

Each configured plugin comes with three configuration options: - *

    - *
  • {@code vocabulary.plugin._plugin_.hierarchy.store = - * # Store entire hierarchy along with selected value. Default: TRUE}
  • - *
  • {@code vocabulary.plugin._plugin_.hierarchy.suggest = - * # Display entire hierarchy in the suggestion list. Default: TRUE}
  • - *
  • {@code vocabulary.plugin._plugin_.delimiter = "" - * # Delimiter to use when building hierarchy strings. Default: "::"}
  • - *
- * } - * - * @author Michael B. Klein - */ - -public class DSpaceControlledVocabulary extends SelfNamedPlugin implements HierarchicalAuthority { - - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); - protected static String xpathTemplate = "//node[contains(translate(@label," + - "'ABCDEFGHIJKLMNOPQRSTUVWXYZÁÀÂÄÃÅĄĂĆČÇĎĐÉÈÊËĘĚÍÌÎÏŁĽŃŇÑÓÒÔÖÕŐŔŘŚŠŞŤŢÚÙÛÜŮŰÝŸŹŽŻ'," + - "'abcdefghijklmnopqrstuvwxyzáàâäãåąăćčçďđéèêëęěíìîïłľńňñóòôöõőŕřśšşťţúùûüůűýÿźžż'),%s)]"; - protected static String idTemplate = "//node[@id = %s]"; - protected static String idTemplateQuoted = "//node[@id = '%s']"; - protected static String labelTemplate = "//node[@label = %s]"; - protected static String idParentTemplate = "//node[@id = '%s']/parent::isComposedBy/parent::node"; - protected static String rootTemplate = "/node"; - protected static String idAttribute = "id"; - protected static String labelAttribute = "label"; - protected static String[] pluginNames = null; - protected String vocabularyName = null; - protected InputSource vocabulary = null; - protected Boolean suggestHierarchy = false; - protected Boolean storeHierarchy = true; - protected String hierarchyDelimiter = "::"; - protected Integer preloadLevel = 1; - protected String valueAttribute = labelAttribute; - protected String valueTemplate = labelTemplate; - - public DSpaceControlledVocabulary() { - super(); - } - - @Override - public boolean storeAuthorityInMetadata() { - // For backward compatibility controlled vocabularies don't store the node id in - // the metadatavalue - return false; - } - - public static String[] getPluginNames() { - if (pluginNames == null) { - initPluginNames(); - } - - return ArrayUtils.clone(pluginNames); - } - - private static synchronized void initPluginNames() { - if (pluginNames == null) { - class xmlFilter implements java.io.FilenameFilter { - @Override - public boolean accept(File dir, String name) { - return name.endsWith(".xml"); - } - } - String vocabulariesPath = DSpaceServicesFactory.getInstance().getConfigurationService() - .getProperty("dspace.dir") + - File.separator + "config" + - File.separator + "controlled-vocabularies"; - String[] xmlFiles = (new File(vocabulariesPath)).list(new xmlFilter()); - List names = new ArrayList<>(); - for (String filename : xmlFiles) { - names.add((new File(filename)).getName().replace(".xml", "")); - } - pluginNames = names.toArray(new String[names.size()]); - log.info("Got plugin names = " + Arrays.deepToString(pluginNames)); - } - } - - protected void init(String locale) { - if (vocabulary == null) { - ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService(); - - log.info("Initializing " + this.getClass().getName()); - vocabularyName = this.getPluginInstanceName(); - String vocabulariesPath = config.getProperty("dspace.dir") + File.separator + "config" + - File.separator + "controlled-vocabularies" + File.separator; - String configurationPrefix = "vocabulary.plugin." + vocabularyName; - storeHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.store", storeHierarchy); - boolean storeIDs = config.getBooleanProperty(configurationPrefix + ".storeIDs", false); - suggestHierarchy = config.getBooleanProperty(configurationPrefix + ".hierarchy.suggest", suggestHierarchy); - preloadLevel = config.getIntProperty(configurationPrefix + ".hierarchy.preloadLevel", preloadLevel); - String configuredDelimiter = config.getProperty(configurationPrefix + ".delimiter"); - if (configuredDelimiter != null) { - hierarchyDelimiter = configuredDelimiter.replaceAll("(^\"|\"$)", ""); - } - if (storeIDs) { - valueAttribute = idAttribute; - valueTemplate = idTemplate; - } - - String filename = vocabulariesPath + vocabularyName + ".xml"; - if (StringUtils.isNotEmpty(locale)) { - String localizedFilename = vocabulariesPath + vocabularyName + "_" + locale + ".xml"; - if (Paths.get(localizedFilename).toFile().exists()) { - filename = localizedFilename; - } - } - log.info("Loading " + filename); - vocabulary = new InputSource(filename); - } - } - - protected String buildString(Node node) { - if (node.getNodeType() == Node.DOCUMENT_NODE || ( - node.getParentNode() != null && - node.getParentNode().getNodeType() == Node.DOCUMENT_NODE)) { - return (""); - } else { - String parentValue = buildString(node.getParentNode()); - Node currentNodeValue = node.getAttributes().getNamedItem(valueAttribute); - if (currentNodeValue != null) { - String currentValue = currentNodeValue.getNodeValue(); - if (parentValue.equals("")) { - return currentValue; - } else { - return (parentValue + this.hierarchyDelimiter + currentValue); - } - } else { - return (parentValue); - } - } - } - - @Override - public Choices getMatches(String text, int start, int limit, String locale) { - init(locale); - log.debug("Getting matches for '" + text + "'"); - String[] textHierarchy = text.split(hierarchyDelimiter, -1); - StringBuilder xpathExpressionBuilder = new StringBuilder(); - for (int i = 0; i < textHierarchy.length; i++) { - xpathExpressionBuilder.append(String.format(xpathTemplate, "$var" + i)); - } - String xpathExpression = xpathExpressionBuilder.toString(); - XPath xpath = XPathFactory.newInstance().newXPath(); - xpath.setXPathVariableResolver(variableName -> { - String varName = variableName.getLocalPart(); - if (varName.startsWith("var")) { - int index = Integer.parseInt(varName.substring(3)); - return textHierarchy[index].toLowerCase(); - } - throw new IllegalArgumentException("Unexpected variable: " + varName); - }); - int total; - List choices; - try { - NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); - total = results.getLength(); - choices = getChoicesFromNodeList(results, start, limit); - } catch (XPathExpressionException e) { - log.warn(e.getMessage(), e); - return new Choices(true); - } - return new Choices(choices.toArray(new Choice[choices.size()]), start, total, Choices.CF_AMBIGUOUS, - total > start + limit); - } - - @Override - public Choices getBestMatch(String text, String locale) { - init(locale); - log.debug("Getting best matches for {}'", text); - String[] textHierarchy = text.split(hierarchyDelimiter, -1); - StringBuilder xpathExpressionBuilder = new StringBuilder(); - for (int i = 0; i < textHierarchy.length; i++) { - xpathExpressionBuilder.append(String.format(valueTemplate, "$var" + i)); - } - String xpathExpression = xpathExpressionBuilder.toString(); - XPath xpath = XPathFactory.newInstance().newXPath(); - xpath.setXPathVariableResolver(variableName -> { - String varName = variableName.getLocalPart(); - if (varName.startsWith("var")) { - int index = Integer.parseInt(varName.substring(3)); - return textHierarchy[index]; - } - throw new IllegalArgumentException("Unexpected variable: " + varName); - }); - List choices; - try { - NodeList results = (NodeList) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODESET); - choices = getChoicesFromNodeList(results, 0, 1); - } catch (XPathExpressionException e) { - log.warn(e.getMessage(), e); - return new Choices(true); - } - return new Choices(choices.toArray(new Choice[choices.size()]), 0, choices.size(), Choices.CF_AMBIGUOUS, false); - } - - @Override - public String getLabel(String key, String locale) { - return getNodeValue(key, locale, this.suggestHierarchy); - } - - @Override - public String getValue(String key, String locale) { - return getNodeValue(key, locale, this.storeHierarchy); - } - - @Override - public Choice getChoice(String authKey, String locale) { - Node node; - try { - node = getNode(authKey, locale); - } catch (XPathExpressionException e) { - return null; - } - return createChoiceFromNode(node); - } - - @Override - public boolean isHierarchical() { - init(null); - return true; - } - - @Override - public Choices getTopChoices(String authorityName, int start, int limit, String locale) { - init(locale); - String xpathExpression = rootTemplate; - return getChoicesByXpath(xpathExpression, start, limit); - } - - @Override - public Choices getChoicesByParent(String authorityName, String parentId, int start, int limit, String locale) { - init(locale); - String xpathExpression = String.format(idTemplateQuoted, parentId); - return getChoicesByXpath(xpathExpression, start, limit); - } - - @Override - public Choice getParentChoice(String authorityName, String childId, String locale) { - init(locale); - try { - String xpathExpression = String.format(idParentTemplate, childId); - Choice choice = createChoiceFromNode(getNodeFromXPath(xpathExpression)); - return choice; - } catch (XPathExpressionException e) { - log.error(e.getMessage(), e); - return null; - } - } - - @Override - public Integer getPreloadLevel() { - init(null); - return preloadLevel; - } - - private boolean isRootElement(Node node) { - return node != null && node.getOwnerDocument().getDocumentElement().equals(node); - } - - private Node getNode(String key, String locale) throws XPathExpressionException { - init(locale); - String xpathExpression = String.format(idTemplateQuoted, key); - Node node = getNodeFromXPath(xpathExpression); - return node; - } - - private Node getNodeFromXPath(String xpathExpression) throws XPathExpressionException { - XPath xpath = XPathFactory.newInstance().newXPath(); - Node node = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); - return node; - } - - private List getChoicesFromNodeList(NodeList results, int start, int limit) { - List choices = new ArrayList<>(); - for (int i = 0; i < results.getLength(); i++) { - if (i < start) { - continue; - } - if (choices.size() == limit) { - break; - } - Node node = results.item(i); - Choice choice = new Choice(getAuthority(node), getLabel(node), getValue(node), - isSelectable(node)); - choice.extras = addOtherInformation(getParent(node), getNote(node), getChildren(node), getAuthority(node)); - choices.add(choice); - } - return choices; - } - - private Map addOtherInformation(String parentCurr, String noteCurr, - List childrenCurr, String authorityCurr) { - Map extras = new HashMap<>(); - if (StringUtils.isNotBlank(parentCurr)) { - extras.put("parent", parentCurr); - } - if (StringUtils.isNotBlank(noteCurr)) { - extras.put("note", noteCurr); - } - if (childrenCurr.isEmpty()) { - extras.put("hasChildren", "false"); - } else { - extras.put("hasChildren", "true"); - } - extras.put("id", authorityCurr); - return extras; - } - - private String getNodeValue(String key, String locale, boolean useHierarchy) { - try { - Node node = getNode(key, locale); - if (Objects.isNull(node)) { - return null; - } - if (useHierarchy) { - return this.buildString(node); - } else { - return node.getAttributes().getNamedItem(valueAttribute).getNodeValue(); - } - } catch (XPathExpressionException e) { - return (""); - } - } - - private String getLabel(Node node) { - String hierarchy = this.buildString(node); - if (this.suggestHierarchy) { - return hierarchy; - } else { - return node.getAttributes().getNamedItem("label").getNodeValue(); - } - } - - private String getValue(Node node) { - String hierarchy = this.buildString(node); - if (this.storeHierarchy) { - return hierarchy; - } else { - return node.getAttributes().getNamedItem(valueAttribute).getNodeValue(); - } - } - - private String getNote(Node node) { - NodeList childNodes = node.getChildNodes(); - for (int ci = 0; ci < childNodes.getLength(); ci++) { - Node firstChild = childNodes.item(ci); - if (firstChild != null && "hasNote".equals(firstChild.getNodeName())) { - String nodeValue = firstChild.getTextContent(); - if (StringUtils.isNotBlank(nodeValue)) { - return nodeValue; - } - } - } - return null; - } - - private List getChildren(Node node) { - List children = new ArrayList<>(); - NodeList childNodes = node.getChildNodes(); - for (int ci = 0; ci < childNodes.getLength(); ci++) { - Node firstChild = childNodes.item(ci); - if (firstChild != null && "isComposedBy".equals(firstChild.getNodeName())) { - for (int cii = 0; cii < firstChild.getChildNodes().getLength(); cii++) { - Node childN = firstChild.getChildNodes().item(cii); - if (childN != null && "node".equals(childN.getNodeName())) { - Node childIdAttr = childN.getAttributes().getNamedItem("id"); - if (null != childIdAttr) { - children.add(childIdAttr.getNodeValue()); - } - } - } - break; - } - } - return children; - } - - private boolean isSelectable(Node node) { - Node selectableAttr = node.getAttributes().getNamedItem("selectable"); - if (null != selectableAttr) { - return Boolean.parseBoolean(selectableAttr.getNodeValue()); - } else { // Default is true - return true; - } - } - - private String getParent(Node node) { - Node parentN = node.getParentNode(); - if (parentN != null) { - parentN = parentN.getParentNode(); - if (parentN != null && !isRootElement(parentN)) { - return buildString(parentN); - } - } - return null; - } - - private String getAuthority(Node node) { - Node idAttr = node.getAttributes().getNamedItem("id"); - if (null != idAttr) { // 'id' is optional - return idAttr.getNodeValue(); - } else { - return null; - } - } - - private Choices getChoicesByXpath(String xpathExpression, int start, int limit) { - List choices = new ArrayList<>(); - XPath xpath = XPathFactory.newInstance().newXPath(); - try { - Node parentNode = (Node) xpath.evaluate(xpathExpression, vocabulary, XPathConstants.NODE); - int count = 0; - if (parentNode != null) { - NodeList childNodes = (NodeList) xpath.evaluate(".//isComposedBy", parentNode, XPathConstants.NODE); - if (null != childNodes) { - for (int i = 0; i < childNodes.getLength(); i++) { - Node childNode = childNodes.item(i); - if (childNode != null && "node".equals(childNode.getNodeName())) { - if (count < start || choices.size() >= limit) { - count++; - continue; - } - count++; - choices.add(createChoiceFromNode(childNode)); - } - } - } - return new Choices(choices.toArray(new Choice[choices.size()]), start, count, - Choices.CF_AMBIGUOUS, false); - } - } catch (XPathExpressionException e) { - log.warn(e.getMessage(), e); - return new Choices(true); - } - return new Choices(false); - } - - private Choice createChoiceFromNode(Node node) { - if (node != null && !isRootElement(node)) { - Choice choice = new Choice(getAuthority(node), getLabel(node), getValue(node), - isSelectable(node)); - choice.extras = addOtherInformation(getParent(node), getNote(node),getChildren(node), getAuthority(node)); - return choice; - } - return null; - } - -} diff --git a/dspace/modules/server/src/main/webapp/.gitignore b/dspace/modules/server/src/main/webapp/.gitignore new file mode 100644 index 000000000000..e69de29bb2d1 From befad94f8a0a14857c9d8f360bd05669570e1837 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 25 Mar 2026 17:39:47 +0100 Subject: [PATCH 649/701] requestItemService.findbyBitstream for more efficient bitstream deletion (cherry picked from commit 85e76a9193f835824775778bad216ac10ad5c555) stripped out the requestitem access token feature, kept some small improvements (interface scope, javadoc typos) --- .../requestitem/RequestItemServiceImpl.java | 6 +++ .../app/requestitem/dao/RequestItemDAO.java | 16 ++++++- .../dao/impl/RequestItemDAOImpl.java | 8 ++++ .../service/RequestItemService.java | 43 +++++++++++++------ .../dspace/content/BitstreamServiceImpl.java | 8 ++-- 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemServiceImpl.java index b915cfedd346..d6d0225655e0 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemServiceImpl.java @@ -11,6 +11,7 @@ import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.UUID; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -96,6 +97,11 @@ public Iterator findByItem(Context context, Item item) throws SQLEx return requestItemDAO.findByItem(context, item); } + @Override + public Iterator findByBitstreamId(Context context, UUID bitstreamId) throws SQLException { + return requestItemDAO.findByBitstreamId(context, bitstreamId); + } + @Override public void update(Context context, RequestItem requestItem) { try { diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/RequestItemDAO.java b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/RequestItemDAO.java index b36ae58e0ca1..9c6954fe6be1 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/RequestItemDAO.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/RequestItemDAO.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.Iterator; +import java.util.UUID; import org.dspace.app.requestitem.RequestItem; import org.dspace.content.Item; @@ -26,7 +27,7 @@ */ public interface RequestItemDAO extends GenericDAO { /** - * Fetch a request named by its unique token (passed in emails). + * Fetch a request named by its unique approval token (passed in emails). * * @param context the current DSpace context. * @param token uniquely identifies the request. @@ -36,4 +37,17 @@ public interface RequestItemDAO extends GenericDAO { public RequestItem findByToken(Context context, String token) throws SQLException; public Iterator findByItem(Context context, Item item) throws SQLException; + + /** + * Retrieve all requests (as iterator) for a given bitstream UUID + * A UUID parameter is used here rather than Bitstream object, to make it usable + * in situations even when a bitstream object no longer exists, but orphaned + * entries need to be found by their (previous) bitstream UUID. + * + * @param context current DSpace context + * @param bitstreamId the bitstream UUID to search for + * @return the matching requests (or empty iterator) + */ + public Iterator findByBitstreamId(Context context, UUID bitstreamId) throws SQLException; + } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java index c76bd50d1910..d6c6c1b231b7 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/dao/impl/RequestItemDAOImpl.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.Iterator; +import java.util.UUID; import jakarta.persistence.Query; import jakarta.persistence.criteria.CriteriaBuilder; @@ -52,4 +53,11 @@ public Iterator findByItem(Context context, Item item) throws SQLEx Query query = createQuery(context, criteriaQuery); return iterate(query); } + + @Override + public Iterator findByBitstreamId(Context context, UUID bitstreamId) throws SQLException { + Query query = createQuery(context, "FROM RequestItem WHERE bitstream.id = :bitstreamId"); + query.setParameter("bitstreamId", bitstreamId); + return iterate(query); + } } diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/service/RequestItemService.java b/dspace-api/src/main/java/org/dspace/app/requestitem/service/RequestItemService.java index efac3b18bc7c..5c27ba9acb55 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/service/RequestItemService.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/service/RequestItemService.java @@ -10,6 +10,7 @@ import java.sql.SQLException; import java.util.Iterator; import java.util.List; +import java.util.UUID; import org.dspace.app.requestitem.RequestItem; import org.dspace.content.Bitstream; @@ -40,7 +41,7 @@ public interface RequestItemService { * @return the token of the request item * @throws SQLException if database error */ - public String createRequest(Context context, Bitstream bitstream, Item item, + String createRequest(Context context, Bitstream bitstream, Item item, boolean allFiles, String reqEmail, String reqName, String reqMessage) throws SQLException; @@ -49,35 +50,51 @@ public String createRequest(Context context, Bitstream bitstream, Item item, * * @param context current DSpace session. * @return all item requests. - * @throws java.sql.SQLException passed through. + * @throws SQLException passed through. */ - public List findAll(Context context) + List findAll(Context context) throws SQLException; /** - * Retrieve a request by its token. + * Retrieve a request by its approver token. * * @param context current DSpace session. - * @param token the token identifying the request. + * @param token the token identifying the request to be approved. * @return the matching request, or null if not found. */ - public RequestItem findByToken(Context context, String token); + RequestItem findByToken(Context context, String token); /** - * Retrieve a request based on the item. + * Retrieve all requests (as iterator) for a given item * @param context current DSpace session. * @param item the item to find requests for. - * @return the matching requests, or null if not found. + * @return the matching requests (or empty iterator) */ - public Iterator findByItem(Context context, Item item) throws SQLException; + Iterator findByItem(Context context, Item item) throws SQLException; + /** - * Save updates to the record. Only accept_request, and decision_date are set-able. + * Retrieve all requests (as iterator) for a given bitstream UUID + * A UUID parameter is used here rather than Bitstream object, to make it usable + * in situations even when a bitstream object no longer exists, but orphaned + * entries need to be found by their (previous) bitstream UUID. + * + * @param context current DSpace context + * @param bitstreamId the bitstream UUID to search for + * @return the matching requests (or empty iterator) + */ + Iterator findByBitstreamId(Context context, UUID bitstreamId) throws SQLException; + + /** + * Save updates to the record. Only accept_request, decision_date, access_period are settable. + * + * Note: the "is settable" rules mentioned here are enforced in RequestItemRest with annotations meaning that + * these JSON properties are considered READ-ONLY by the core DSpaceRestRepository methods * * @param context The relevant DSpace Context. * @param requestItem requested item */ - public void update(Context context, RequestItem requestItem); + void update(Context context, RequestItem requestItem); /** * Remove the record from the database. @@ -85,7 +102,7 @@ public List findAll(Context context) * @param context current DSpace context. * @param request record to be removed. */ - public void delete(Context context, RequestItem request); + void delete(Context context, RequestItem request); /** * Is there at least one valid READ resource policy for this object? @@ -94,6 +111,6 @@ public List findAll(Context context) * @return true if a READ policy applies. * @throws SQLException passed through. */ - public boolean isRestricted(Context context, DSpaceObject o) + boolean isRestricted(Context context, DSpaceObject o) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index 3a2670395dbe..eecb0711ead5 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -293,11 +293,9 @@ public void delete(Context context, Bitstream bitstream) throws SQLException, Au // Remove any RequestItem entities associated with this bitstream ensuring there are no requests referencing // a deleted bitstream - List requestItems = requestItemService.findAll(context); - for (RequestItem requestItem : requestItems) { - if (bitstream.equals(requestItem.getBitstream())) { - requestItemService.delete(context, requestItem); - } + Iterator requestItems = requestItemService.findByBitstreamId(context, bitstream.getID()); + while (requestItems.hasNext()) { + requestItemService.delete(context, requestItems.next()); } // Remove policies only after the bitstream has been updated (otherwise the current user has not WRITE rights) From 18b55dae2a621741f65f09d157bb612a412b6b3c Mon Sep 17 00:00:00 2001 From: bram-atmire Date: Wed, 25 Mar 2026 20:19:09 +0100 Subject: [PATCH 650/701] Add IT coverage for findByBitstreamId and bitstream deletion cleanup - Add testFindByBitstreamId: verifies the new findByBitstreamId method returns matching requests, returns empty for bitstreams without requests, and returns empty for nonexistent UUIDs. - Enhance testDeleteBitstreamRemovesRequestItem: also verify via findByBitstreamId both before and after deletion. (cherry picked from commit 11165705b9982498c1672f95968e79317c2fd8d2) --- .../app/rest/RequestItemRepositoryIT.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java index 3a7c805cacc6..f1ab2c1c4e3a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RequestItemRepositoryIT.java @@ -668,11 +668,56 @@ public void testDeleteBitstreamRemovesRequestItem() throws Exception { // verify the body is empty .andExpect(jsonPath("$").doesNotExist()); + // Verify the request item exists via findByBitstreamId before deletion + Iterator bitstreamRequests = requestItemService.findByBitstreamId(context, bitstream.getID()); + assertTrue("Request item should exist before bitstream deletion", bitstreamRequests.hasNext()); + // Delete associated Bitstream ContentServiceFactory.getInstance().getBitstreamService().delete(context, bitstream); // Verify that all RequestItems related to this bitstream have been removed Iterator itemRequests = requestItemService.findByItem(context, item); assertFalse(itemRequests.hasNext()); + + // Also verify via findByBitstreamId + Iterator remaining = requestItemService.findByBitstreamId(context, bitstream.getID()); + assertFalse("Request items should be removed after bitstream deletion", remaining.hasNext()); + } + + /** + * Test that findByBitstreamId returns matching request items and does not return items for other bitstreams. + */ + @Test + public void testFindByBitstreamId() throws Exception { + context.turnOffAuthorisationSystem(); + + // Create a request item for the existing bitstream + RequestItemBuilder.createRequestItem(context, item, bitstream) + .build(); + + // Create a second bitstream with no request items + InputStream is2 = new ByteArrayInputStream("other content".getBytes()); + Bitstream bitstream2 = BitstreamBuilder + .createBitstream(context, item, is2) + .withName("Other Bitstream") + .build(); + + context.restoreAuthSystemState(); + + // findByBitstreamId should return the request for the first bitstream + Iterator results = requestItemService.findByBitstreamId(context, bitstream.getID()); + assertTrue("Should find request item for bitstream", results.hasNext()); + RequestItem found = results.next(); + assertEquals("Request item should reference correct bitstream", + bitstream.getID(), found.getBitstream().getID()); + assertFalse("Should only find one request item", results.hasNext()); + + // findByBitstreamId should return nothing for the second bitstream + Iterator noResults = requestItemService.findByBitstreamId(context, bitstream2.getID()); + assertFalse("Should find no request items for bitstream without requests", noResults.hasNext()); + + // findByBitstreamId should return nothing for a random UUID + Iterator randomResults = requestItemService.findByBitstreamId(context, UUID.randomUUID()); + assertFalse("Should find no request items for nonexistent bitstream", randomResults.hasNext()); } } From 190fb67b29d3d39da5ec0022c24eeb91ff66190b Mon Sep 17 00:00:00 2001 From: bram-atmire Date: Sun, 17 May 2026 12:55:26 +0200 Subject: [PATCH 651/701] Fix dspace cleanup FK violations from orphaned bundle2bitstream rows The `dspace cleanup` command fails with FK constraint violations when deleted bitstreams still have orphaned rows in bundle2bitstream. The soft-delete path (delete()) normally cleans these up, but historical bugs left orphans that block hard-delete via expunge(). Changes: * BitstreamServiceImpl.expunge() defensively removes bundle2bitstream, request item, and resource policy references before hard-delete. * BitstreamStorageServiceImpl.cleanup() moves the isRegisteredBitstream and findDuplicateInternalIdentifier checks before expunge(), because expunge() makes the bitstream entity stale for subsequent queries. * New Flyway migration V8.4_2026.03.28 cleans up existing orphan rows on upgrade. Named V8.4 so it backports cleanly to 8.x and 9.x. * New BitstreamExpungeIT verifies expunge() succeeds on a soft-deleted bitstream without FK violations. (cherry picked from commit bacd38e737f486b7acbab63bef4093944c3553e2) --- .../dspace/content/BitstreamServiceImpl.java | 22 ++ .../bitstore/BitstreamStorageServiceImpl.java | 20 +- ...ndle2bitstream-with-deleted-bitstreams.sql | 17 ++ .../dspace/content/BitstreamExpungeIT.java | 247 ++++++++++++++++++ 4 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.4_2026.03.28__Fix-bundle2bitstream-with-deleted-bitstreams.sql create mode 100644 dspace-api/src/test/java/org/dspace/content/BitstreamExpungeIT.java diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index eecb0711ead5..7ab02d44e558 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -359,6 +359,28 @@ public void expunge(Context context, Bitstream bitstream) throws SQLException, A throw new IllegalStateException("Bitstream " + bitstream.getID().toString() + " must be deleted before it can be removed from the database."); } + + // Defensively remove any remaining bundle2bitstream references. + // Normally delete() already cleans these up, but orphaned rows from + // historical bugs can cause FK constraint violations on hard-delete. + final List bundles = bitstream.getBundles(); + for (Bundle bundle : bundles) { + if (bitstream.equals(bundle.getPrimaryBitstream())) { + bundle.unsetPrimaryBitstreamID(); + } + bundle.removeBitstream(bitstream); + } + bundles.clear(); + + // Remove any orphaned request items referencing this bitstream + Iterator requestItems = requestItemService.findByBitstreamId(context, bitstream.getID()); + while (requestItems.hasNext()) { + requestItemService.delete(context, requestItems.next()); + } + + // Remove any remaining authorization policies + authorizeService.removeAllPolicies(context, bitstream); + bitstreamDAO.delete(context, bitstream); } diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java index 9ebf3b29849c..43427dae3bbc 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java @@ -270,6 +270,13 @@ public void cleanup(boolean deleteDbRecords, boolean verbose) throws SQLExceptio continue; } + // Check whether the bitstore file should be kept before + // expunging the database record, because expunge() would + // make the bitstream entity stale for subsequent queries. + boolean isRegistered = isRegisteredBitstream(bitstream.getInternalId()); + boolean hasDuplicate = !bitstreamService + .findDuplicateInternalIdentifier(context, bitstream).isEmpty(); + if (deleteDbRecords) { log.debug("deleting db record"); if (verbose) { @@ -282,16 +289,15 @@ public void cleanup(boolean deleteDbRecords, boolean verbose) throws SQLExceptio bitstreamService.expunge(context, bitstream); } - if (isRegisteredBitstream(bitstream.getInternalId())) { + if (isRegistered) { context.uncacheEntity(bitstream); - continue; // do not delete registered bitstreams + continue; // do not delete registered bitstreams from the bitstore } - - // Since versioning allows for multiple bitstreams, check if the internal - // identifier isn't used on - // another place - if (bitstreamService.findDuplicateInternalIdentifier(context, bitstream).isEmpty()) { + // Since versioning allows for multiple bitstreams, only + // remove the file if no other bitstream shares this + // internal identifier + if (!hasDuplicate) { this.getStore(bitstream.getStoreNumber()).remove(bitstream); String message = ("Deleted bitstreamID " + bid + ", internalID " + bitstream.getInternalId()); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.4_2026.03.28__Fix-bundle2bitstream-with-deleted-bitstreams.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.4_2026.03.28__Fix-bundle2bitstream-with-deleted-bitstreams.sql new file mode 100644 index 000000000000..fa37ac35e81a --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.4_2026.03.28__Fix-bundle2bitstream-with-deleted-bitstreams.sql @@ -0,0 +1,17 @@ +-- +-- 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/ +-- + +-- Remove orphaned bundle2bitstream rows that reference deleted bitstreams. +-- These orphaned rows prevent 'dspace cleanup' from expunging deleted +-- bitstreams due to FK constraint violations. +DELETE +FROM bundle2bitstream +WHERE bitstream_id IN + (SELECT bs.uuid + FROM bitstream AS bs + WHERE bs.deleted IS TRUE) diff --git a/dspace-api/src/test/java/org/dspace/content/BitstreamExpungeIT.java b/dspace-api/src/test/java/org/dspace/content/BitstreamExpungeIT.java new file mode 100644 index 000000000000..284ecabacc68 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/content/BitstreamExpungeIT.java @@ -0,0 +1,247 @@ +/** + * 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.content; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.InputStream; +import java.util.Iterator; +import java.util.UUID; + +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.requestitem.RequestItem; +import org.dspace.app.requestitem.factory.RequestItemServiceFactory; +import org.dspace.app.requestitem.service.RequestItemService; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.RequestItemBuilder; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.BundleService; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for {@link BitstreamServiceImpl#expunge}. + * + * Verifies that expunge() defensively cleans up bundle2bitstream, requestitem, + * and resourcepolicy references before the hard-delete, so historical orphaned + * rows do not cause FK constraint violations. + * + * @author Bram Luyten (bram at atmire.com) + */ +public class BitstreamExpungeIT extends AbstractIntegrationTestWithDatabase { + + private final BitstreamService bitstreamService = + ContentServiceFactory.getInstance().getBitstreamService(); + private final BundleService bundleService = + ContentServiceFactory.getInstance().getBundleService(); + private final RequestItemService requestItemService = + RequestItemServiceFactory.getInstance().getRequestItemService(); + private final AuthorizeService authorizeService = + AuthorizeServiceFactory.getInstance().getAuthorizeService(); + + private Collection collection; + + @Before + public void setup() { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).build(); + collection = CollectionBuilder.createCollection(context, parentCommunity).build(); + context.restoreAuthSystemState(); + } + + /** + * Smoke test the normal path: a bitstream that has been soft-deleted via + * delete() can be expunged. delete() already cleans up FK references, so + * expunge() should succeed without any defensive work. + */ + @Test + public void testExpungeAfterNormalDelete() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + + bitstreamService.delete(context, bitstream); + context.commit(); + + bitstream = bitstreamService.find(context, bitstreamId); + assertTrue("Bitstream should be marked as deleted", bitstream.isDeleted()); + + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should not exist after expunge", + bitstreamService.find(context, bitstreamId)); + + context.restoreAuthSystemState(); + } + + /** + * Simulate the historical bug scenario: a bitstream is marked deleted but + * its bundle2bitstream row was never cleaned up. expunge() must remove the + * leftover bundle association so the hard-delete does not hit a FK + * constraint violation. + */ + @Test + public void testExpungeOrphanedBundleReference() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + UUID bundleId = bitstream.getBundles().get(0).getID(); + + // Mark the bitstream as deleted WITHOUT going through delete(), so the + // bundle relationship stays in place. This is the orphan state the PR + // is designed to recover from. + bitstream.setDeleted(true); + bitstreamService.update(context, bitstream); + context.commit(); + + bitstream = bitstreamService.find(context, bitstreamId); + assertEquals("Bundle association should still exist before expunge", + 1, bitstream.getBundles().size()); + + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should be removed after expunge", + bitstreamService.find(context, bitstreamId)); + + Bundle bundle = bundleService.find(context, bundleId); + assertFalse("Bundle should no longer reference the bitstream", + bundle.getBitstreams().stream().anyMatch(b -> b.getID().equals(bitstreamId))); + + context.restoreAuthSystemState(); + } + + /** + * Variant where the orphaned bitstream is also the bundle's primary + * bitstream. expunge() must unset the primary bitstream pointer before + * hard-deleting, otherwise the bundle_primary_bitstream_id_fkey constraint + * fires. + */ + @Test + public void testExpungeOrphanedPrimaryBitstream() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + Bundle bundle = bitstream.getBundles().get(0); + UUID bundleId = bundle.getID(); + + bundle.setPrimaryBitstreamID(bitstream); + bundleService.update(context, bundle); + + bitstream.setDeleted(true); + bitstreamService.update(context, bitstream); + context.commit(); + + // Re-fetch in a fresh session state, simulating the cleanup script + // path which iterates deleted bitstreams in a separate session. + bitstream = bitstreamService.find(context, bitstreamId); + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should be removed after expunge", + bitstreamService.find(context, bitstreamId)); + assertNull("Bundle should no longer have a primary bitstream", + bundleService.find(context, bundleId).getPrimaryBitstream()); + + context.restoreAuthSystemState(); + } + + /** + * Variant where a RequestItem references the orphaned bitstream. expunge() + * must remove the request item so the requestitem_bitstream_id_fkey + * constraint does not fire on the hard-delete. + */ + @Test + public void testExpungeOrphanedRequestItem() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + + RequestItemBuilder.createRequestItem(context, item, bitstream).build(); + + bitstream.setDeleted(true); + bitstreamService.update(context, bitstream); + context.commit(); + + Iterator before = requestItemService.findByBitstreamId(context, bitstreamId); + assertTrue("RequestItem should exist before expunge", before.hasNext()); + + bitstream = bitstreamService.find(context, bitstreamId); + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should be removed after expunge", + bitstreamService.find(context, bitstreamId)); + assertFalse("RequestItem should be removed after expunge", + requestItemService.findByBitstreamId(context, bitstreamId).hasNext()); + + context.restoreAuthSystemState(); + } + + /** + * Verify that policies attached to the bitstream are removed by expunge(), + * so resource policy rows do not survive the hard-delete. + */ + @Test + public void testExpungeRemovesPolicies() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, collection).withTitle("Test item").build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, toInputStream("test content")) + .build(); + UUID bitstreamId = bitstream.getID(); + + assertFalse("Bitstream should have inherited policies", + authorizeService.getPolicies(context, bitstream).isEmpty()); + + bitstream.setDeleted(true); + bitstreamService.update(context, bitstream); + context.commit(); + + bitstream = bitstreamService.find(context, bitstreamId); + bitstreamService.expunge(context, bitstream); + context.commit(); + + assertNull("Bitstream should be removed after expunge", + bitstreamService.find(context, bitstreamId)); + + context.restoreAuthSystemState(); + } + + private InputStream toInputStream(String content) { + return IOUtils.toInputStream(content, "UTF-8"); + } +} From 76a109e0ec7e7a32dc6c173676a0dd2372969689 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 15 May 2026 13:32:42 +0200 Subject: [PATCH 652/701] feat: add memory usage factor configuration for S3 transfers and adjust maxConcurrency logic ref: DURACOM-484 (cherry picked from commit e649dd1c4ed8c44b1d7a4e2aa5cb1789fc710bf6) --- .../storage/bitstore/S3BitStoreService.java | 75 ++++++++++++++----- dspace/config/modules/assetstore.cfg | 15 +++- dspace/config/spring/api/bitstore.xml | 7 +- 3 files changed, 72 insertions(+), 25 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index 4ef6915f3c27..1897f38fd471 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -93,7 +93,8 @@ public class S3BitStoreService extends BaseBitStoreService { private double targetThroughputGbps = 10.0; private long minPartSizeBytes = 8 * 1024 * 1024L; private ChecksumAlgorithm s3ChecksumAlgorithm = ChecksumAlgorithm.CRC32; - private Integer maxConcurrency = null; + private Integer maxConcurrency; + private double memoryUsageFactor = 0.1; /** * container for all the assets @@ -116,21 +117,23 @@ public class S3BitStoreService extends BaseBitStoreService { /** * Utility method for generate AmazonS3 builder * - * @param regions wanted regions in client - * @param awsCredentials credentials of the client + * @param region wanted regions in client + * @param credentialsProvider credentials of the client * @param endpoint custom AWS endpoint * @param targetThroughput target throughput in Gbps * @param minPartSize minimum part size in bytes - * @param maxConcurrency maximum number of concurrent requests + * @param concurrency maximum number of concurrent requests + * @param memoryUsageFactor factor to determine memory usage for read buffer size, as a percentage of max memory. * @return builder with the specified parameters */ protected static Supplier amazonClientBuilderBy( - Region region, - AwsCredentialsProvider credentialsProvider, - String endpoint, - double targetThroughput, - long minPartSize, - Integer maxConcurrency + Region region, + AwsCredentialsProvider credentialsProvider, + String endpoint, + double targetThroughput, + long minPartSize, + Integer concurrency, + double memoryUsageFactor ) { return () -> { S3CrtAsyncClientBuilder crtBuilder = S3AsyncClient.crtBuilder(); @@ -143,16 +146,41 @@ protected static Supplier amazonClientBuilderBy( crtBuilder.region(region); } - if (maxConcurrency != null) { - crtBuilder.maxConcurrency(maxConcurrency); - } - if (StringUtils.isNotBlank(endpoint)) { crtBuilder.endpointOverride(URI.create(endpoint)); crtBuilder.forcePathStyle(true); } - return crtBuilder.targetThroughputInGbps(targetThroughput).minimumPartSizeInBytes(minPartSize).build(); + int maxConcurrency; + if (concurrency == null) { + log.warn("maxConcurrency is not set, defaulting to number of available processors"); + maxConcurrency = Runtime.getRuntime().availableProcessors(); + } else { + maxConcurrency = concurrency; + } + + final long maxMemory = Math.round(Math.floor(Runtime.getRuntime().maxMemory() * memoryUsageFactor)); + final long readBuffer = Math.round( + Math.floor((((double) maxMemory / maxConcurrency / minPartSize) - 1) * minPartSize) + ); + + final long maxReadBuffer; + if (readBuffer < minPartSize) { + log.warn( + "Calculated read buffer size is less than the minimum part size. Adjusting to minimum part size." + ); + maxReadBuffer = minPartSize; + maxConcurrency = Math.max((int) Math.floor((double) maxMemory / (maxReadBuffer + minPartSize)), 1); + log.warn("Adjusted maxConcurrency to {} to fit memory constraints.", maxConcurrency); + } else { + maxReadBuffer = readBuffer; + } + + return crtBuilder.maxConcurrency(maxConcurrency) + .targetThroughputInGbps(targetThroughput) + .minimumPartSizeInBytes(minPartSize) + .initialReadBufferSizeInBytes(maxReadBuffer) + .build(); }; } @@ -202,10 +230,10 @@ public void init() throws IOException { s3AsyncClient = FunctionalUtils.getDefaultOrBuild( this.s3AsyncClient, amazonClientBuilderBy( - region, - StaticCredentialsProvider.create(AwsBasicCredentials.create(getAwsAccessKey(), + region, + StaticCredentialsProvider.create(AwsBasicCredentials.create(getAwsAccessKey(), getAwsSecretKey())), endpoint, targetThroughputGbps, - minPartSizeBytes, maxConcurrency) + minPartSizeBytes, maxConcurrency, memoryUsageFactor) ); log.warn("S3 Region set to: " + region.id()); } else { @@ -213,7 +241,7 @@ public void init() throws IOException { s3AsyncClient = FunctionalUtils.getDefaultOrBuild( this.s3AsyncClient, amazonClientBuilderBy(null, null , endpoint, targetThroughputGbps, - minPartSizeBytes, maxConcurrency)); + minPartSizeBytes, maxConcurrency, memoryUsageFactor)); } // bucket name @@ -553,6 +581,15 @@ public void setEndpoint(String endpoint) { this.endpoint = endpoint; } + public double getMemoryUsageFactor() { + return memoryUsageFactor; + } + + public S3BitStoreService setMemoryUsageFactor(double memoryUsageFactor) { + this.memoryUsageFactor = memoryUsageFactor; + return this; + } + /** * Contains a command-line testing tool. Expects arguments: * -a accessKey -s secretKey -f assetFileName diff --git a/dspace/config/modules/assetstore.cfg b/dspace/config/modules/assetstore.cfg index 74989aad0e5a..15541ad3f816 100644 --- a/dspace/config/modules/assetstore.cfg +++ b/dspace/config/modules/assetstore.cfg @@ -65,13 +65,20 @@ assetstore.s3.awsRegionName = # The target throughput for transfer requests in Gbps. Higher value means more connections will be established with S3. assetstore.s3.targetThroughputGbps = 10.0 -# Sets the minimum part size for transfer parts. Decreasing the minimum part size causes multipart transfer to be split -# into a larger number of smaller parts. +# Sets the minimum part size for transfer parts. +# Decreasing the minimum part size causes multipart transfer to be split into a larger number of smaller parts. +# Increase also the number of maxConcurrency if you want to maxout the throughput with smaller part sizes. assetstore.s3.minPartSizeBytes = 8388608 # Specifies the maximum number of S3 connections that should be established during a transfer. -# If not provided, it will be based on targetThroughputGbps -assetstore.s3.maxConcurrency = +# If not provided, it will be used a proper value depending on the machine capabilities (CPU cores and available memory). +# assetstore.s3.maxConcurrency = + +# The amount of maximum memory used during data transfers, as a percentage of the total available memory. +# Default is 0.1 (10% of total memory - Xmx). Adjust this value based on the available memory and expected transfer sizes. +# This value should be set between 0 and 1, where 0 means no memory usage limit and 1 means using all available memory. +# BE careful with this value, since it can cause OOM - especially for lower Xmx values ( 256m / 512m ) +# assetstore.s3.memoryUsageFactor=0.1 # The algorithm the S3 client will use to create a checksum when doing putObject. assetstore.s3.s3ChecksumAlgorithm = CRC32 diff --git a/dspace/config/spring/api/bitstore.xml b/dspace/config/spring/api/bitstore.xml index af50a4c16fc3..11a6ea2a71ba 100644 --- a/dspace/config/spring/api/bitstore.xml +++ b/dspace/config/spring/api/bitstore.xml @@ -47,9 +47,12 @@ - + + + + From 24d437a99d4ce15bd3457af20a0c790bd61e11ff Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Tue, 19 May 2026 09:31:28 +0200 Subject: [PATCH 653/701] feat: enhance S3 transfer configuration with memory usage factor and initial read buffer size ref: DURACOM-484 (cherry picked from commit a4806d8f9321d97842b60e0b4c319970bc5c9dd7) --- .../storage/bitstore/S3BitStoreService.java | 78 +++++++++++++++---- dspace/config/modules/assetstore.cfg | 11 ++- dspace/config/spring/api/bitstore.xml | 11 ++- 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java index 1897f38fd471..ac1f147f41b4 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/S3BitStoreService.java @@ -94,7 +94,8 @@ public class S3BitStoreService extends BaseBitStoreService { private long minPartSizeBytes = 8 * 1024 * 1024L; private ChecksumAlgorithm s3ChecksumAlgorithm = ChecksumAlgorithm.CRC32; private Integer maxConcurrency; - private double memoryUsageFactor = 0.1; + private Double memoryUsageFactor; + private Long initialReadBufferSizeInBytes; /** * container for all the assets @@ -124,6 +125,8 @@ public class S3BitStoreService extends BaseBitStoreService { * @param minPartSize minimum part size in bytes * @param concurrency maximum number of concurrent requests * @param memoryUsageFactor factor to determine memory usage for read buffer size, as a percentage of max memory. + * @param initialReadBufferSizeInBytes initial read buffer size in bytes, + * used if memoryUsageFactor is not set or out of bounds * @return builder with the specified parameters */ protected static Supplier amazonClientBuilderBy( @@ -133,10 +136,14 @@ protected static Supplier amazonClientBuilderBy( double targetThroughput, long minPartSize, Integer concurrency, - double memoryUsageFactor + Double memoryUsageFactor, + Long initialReadBufferSizeInBytes ) { return () -> { - S3CrtAsyncClientBuilder crtBuilder = S3AsyncClient.crtBuilder(); + S3CrtAsyncClientBuilder crtBuilder = + S3AsyncClient.crtBuilder() + .targetThroughputInGbps(targetThroughput) + .minimumPartSizeInBytes(minPartSize); if (credentialsProvider != null) { crtBuilder.credentialsProvider(credentialsProvider); @@ -151,6 +158,20 @@ protected static Supplier amazonClientBuilderBy( crtBuilder.forcePathStyle(true); } + if (memoryUsageFactor == null) { + log.warn("custom heuristic cannot be applied!"); + + if (initialReadBufferSizeInBytes != null) { + crtBuilder.initialReadBufferSizeInBytes(initialReadBufferSizeInBytes); + } + + if (concurrency != null) { + crtBuilder.maxConcurrency(concurrency); + } + + return crtBuilder.build(); + } + int maxConcurrency; if (concurrency == null) { log.warn("maxConcurrency is not set, defaulting to number of available processors"); @@ -159,7 +180,14 @@ protected static Supplier amazonClientBuilderBy( maxConcurrency = concurrency; } - final long maxMemory = Math.round(Math.floor(Runtime.getRuntime().maxMemory() * memoryUsageFactor)); + long maxMemory; + if (memoryUsageFactor <= 0 || memoryUsageFactor > 1) { + log.warn("memoryUsageFactor is not set or out of bounds (0,1], defaulting to 0.1"); + maxMemory = Math.round(Math.floor(Runtime.getRuntime().maxMemory() * 0.1)); + } else { + maxMemory = Math.round(Math.floor(Runtime.getRuntime().maxMemory() * memoryUsageFactor)); + } + final long readBuffer = Math.round( Math.floor((((double) maxMemory / maxConcurrency / minPartSize) - 1) * minPartSize) ); @@ -167,7 +195,8 @@ protected static Supplier amazonClientBuilderBy( final long maxReadBuffer; if (readBuffer < minPartSize) { log.warn( - "Calculated read buffer size is less than the minimum part size. Adjusting to minimum part size." + "Calculated read buffer size is less than the minimum part size. Adjusting to minimum part " + + "size." ); maxReadBuffer = minPartSize; maxConcurrency = Math.max((int) Math.floor((double) maxMemory / (maxReadBuffer + minPartSize)), 1); @@ -176,9 +205,13 @@ protected static Supplier amazonClientBuilderBy( maxReadBuffer = readBuffer; } + log.info( + "Calculated read buffer size: {} bytes, max concurrency: {}, based on memory usage factor: {} " + + "and max memory: {} bytes", + maxReadBuffer, maxConcurrency, memoryUsageFactor, maxMemory + ); + return crtBuilder.maxConcurrency(maxConcurrency) - .targetThroughputInGbps(targetThroughput) - .minimumPartSizeInBytes(minPartSize) .initialReadBufferSizeInBytes(maxReadBuffer) .build(); }; @@ -231,17 +264,23 @@ public void init() throws IOException { this.s3AsyncClient, amazonClientBuilderBy( region, - StaticCredentialsProvider.create(AwsBasicCredentials.create(getAwsAccessKey(), - getAwsSecretKey())), endpoint, targetThroughputGbps, - minPartSizeBytes, maxConcurrency, memoryUsageFactor) - ); + StaticCredentialsProvider.create( + AwsBasicCredentials.create(getAwsAccessKey(), getAwsSecretKey()) + ), + endpoint, targetThroughputGbps, minPartSizeBytes, maxConcurrency, memoryUsageFactor, + initialReadBufferSizeInBytes + ) + ); log.warn("S3 Region set to: " + region.id()); } else { log.info("Using a IAM role or aws environment credentials"); s3AsyncClient = FunctionalUtils.getDefaultOrBuild( this.s3AsyncClient, - amazonClientBuilderBy(null, null , endpoint, targetThroughputGbps, - minPartSizeBytes, maxConcurrency, memoryUsageFactor)); + amazonClientBuilderBy( + null, null, endpoint, targetThroughputGbps, + minPartSizeBytes, maxConcurrency, memoryUsageFactor, initialReadBufferSizeInBytes + ) + ); } // bucket name @@ -581,13 +620,20 @@ public void setEndpoint(String endpoint) { this.endpoint = endpoint; } - public double getMemoryUsageFactor() { + public Double getMemoryUsageFactor() { return memoryUsageFactor; } - public S3BitStoreService setMemoryUsageFactor(double memoryUsageFactor) { + public void setMemoryUsageFactor(Double memoryUsageFactor) { this.memoryUsageFactor = memoryUsageFactor; - return this; + } + + public Long getInitialReadBufferSizeInBytes() { + return initialReadBufferSizeInBytes; + } + + public void setInitialReadBufferSizeInBytes(Long initialReadBufferSizeInBytes) { + this.initialReadBufferSizeInBytes = initialReadBufferSizeInBytes; } /** diff --git a/dspace/config/modules/assetstore.cfg b/dspace/config/modules/assetstore.cfg index 15541ad3f816..5951e291e950 100644 --- a/dspace/config/modules/assetstore.cfg +++ b/dspace/config/modules/assetstore.cfg @@ -78,7 +78,16 @@ assetstore.s3.minPartSizeBytes = 8388608 # Default is 0.1 (10% of total memory - Xmx). Adjust this value based on the available memory and expected transfer sizes. # This value should be set between 0 and 1, where 0 means no memory usage limit and 1 means using all available memory. # BE careful with this value, since it can cause OOM - especially for lower Xmx values ( 256m / 512m ) -# assetstore.s3.memoryUsageFactor=0.1 +# If not set, there is no memory usage limit and the S3 client will use as much memory as needed for transfers, +# which may lead to OutOfMemory errors if the transfer sizes are large and the available memory is limited. +assetstore.s3.memoryUsageFactor=0.1 + + +# The size of the buffer used for reading data from S3 during transfers, in bytes. +# Default is 16KB (16384 bytes). Adjust this value based on the expected transfer sizes and available memory. +# A larger buffer size may improve performance for larger transfers, but it also increases memory usage. +# If not set, the S3 client will use a default buffer size of 16KB (16384 bytes) for reading data from S3 during transfers. +#assetstore.s3.initialReadBufferSizeInBytes= # The algorithm the S3 client will use to create a checksum when doing putObject. assetstore.s3.s3ChecksumAlgorithm = CRC32 diff --git a/dspace/config/spring/api/bitstore.xml b/dspace/config/spring/api/bitstore.xml index 11a6ea2a71ba..84fdc8282204 100644 --- a/dspace/config/spring/api/bitstore.xml +++ b/dspace/config/spring/api/bitstore.xml @@ -51,8 +51,15 @@ --> - - + + + + + From dbe502160a9b4fb3778c35e73457855766bfbdee Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Tue, 19 May 2026 12:57:22 +0200 Subject: [PATCH 654/701] Fix missing OAI identifier interpolation on config change When dspace.cfg is updated while Tomcat is running, the oai.identifier.prefix fallback from dspace-oai's DSpaceConfigurationService gets cleared out. If the cached Identify request gets invalidated after this, it will be regenerated with ${oai.identifier.prefix} in place We re-initialize this fallback when re-creating the Identify content to account for this. This is the only place where setProperty is used outside of tests. (cherry picked from commit 263513bcbaffae00d1cbaf1f5237b0f05738a76d) --- .../api/config/ConfigurationService.java | 2 ++ .../impl/config/DSpaceConfigurationService.java | 16 +++++++++++----- .../impl/xoai/DSpaceRepositoryConfiguration.java | 1 + .../dspace/services/ConfigurationService.java | 2 ++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/services/api/config/ConfigurationService.java b/dspace-oai/src/main/java/org/dspace/xoai/services/api/config/ConfigurationService.java index bc6083166baf..997e08adec83 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/services/api/config/ConfigurationService.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/services/api/config/ConfigurationService.java @@ -15,4 +15,6 @@ public interface ConfigurationService { boolean getBooleanProperty(String module, String key, boolean defaultValue); boolean getBooleanProperty(String key, boolean defaultValue); + + void ensureRequiredConfiguration(); } diff --git a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/config/DSpaceConfigurationService.java b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/config/DSpaceConfigurationService.java index 67d4f09f5c88..0fb0c51defd3 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/config/DSpaceConfigurationService.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/config/DSpaceConfigurationService.java @@ -20,12 +20,18 @@ public class DSpaceConfigurationService implements ConfigurationService { * Initialize the OAI Configuration Service */ public DSpaceConfigurationService() { - // Check the DSpace ConfigurationService for required OAI-PMH settings. - // If they do not exist, set sane defaults as needed. + ensureRequiredConfiguration(); + } - // Per OAI Spec, "oai.identifier.prefix" should be the hostname / domain name of the site. - // This configuration is needed by the [dspace]/config/crosswalks/oai/description.xml template, so if - // unspecified we will dynamically set it to the hostname of the "dspace.ui.url" configuration. + /** + * Check the DSpace ConfigurationService for required OAI-PMH settings. + * If they do not exist, set sane defaults as needed. + *
+ * Per OAI Spec, "oai.identifier.prefix" should be the hostname / domain name of the site. + * This configuration is needed by the [dspace]/config/crosswalks/oai/description.xml template, so if + * unspecified we will dynamically set it to the hostname of the "dspace.ui.url" configuration. + */ + public void ensureRequiredConfiguration() { if (!configurationService.hasProperty("oai.identifier.prefix")) { configurationService.setProperty("oai.identifier.prefix", Utils.getHostName(configurationService.getProperty("dspace.ui.url"))); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceRepositoryConfiguration.java b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceRepositoryConfiguration.java index 8c9841dfb949..38b354f05919 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceRepositoryConfiguration.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceRepositoryConfiguration.java @@ -133,6 +133,7 @@ public String getRepositoryName() { @Override public List getDescription() { + configurationService.ensureRequiredConfiguration(); List result = new ArrayList(); String descriptionFile = configurationService.getProperty("oai.description.file"); if (descriptionFile == null) { diff --git a/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java b/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java index 526a518a0911..7681553d4168 100644 --- a/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java +++ b/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java @@ -251,6 +251,8 @@ public interface ConfigurationService { * Set a configuration property (setting) in the system. * Type is not important here since conversion happens automatically * when properties are requested. + *
+ * Note: use with care, the value will be reset when the configuration is reloaded! * * @param name the property name * @param value the property value (set this to null to clear out the property) From b7060333d3e215eed7750f7d207bbe9059c19a6a Mon Sep 17 00:00:00 2001 From: abhinav Date: Tue, 20 May 2025 17:14:36 +0200 Subject: [PATCH 655/701] Improve caching for entities and relationship types (cherry picked from commit 11c06f21e694af5c7d7f630c2033fb2256f1a646) --- .../main/java/org/dspace/content/EntityType.java | 5 +++++ .../java/org/dspace/content/RelationshipType.java | 5 +++++ .../dspace/content/dao/impl/EntityTypeDAOImpl.java | 2 +- .../content/dao/impl/RelationshipTypeDAOImpl.java | 6 +++--- .../h2/V7.6_2025.05.20__entity_types_caching.sql | 9 +++++++++ .../V7.6_2025.05.20__entity_types_caching.sql | 9 +++++++++ dspace/config/hibernate-ehcache-config.xml | 14 ++++++++++++++ 7 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.05.20__entity_types_caching.sql diff --git a/dspace-api/src/main/java/org/dspace/content/EntityType.java b/dspace-api/src/main/java/org/dspace/content/EntityType.java index 720e0c492ca7..c46cd056eb6f 100644 --- a/dspace-api/src/main/java/org/dspace/content/EntityType.java +++ b/dspace-api/src/main/java/org/dspace/content/EntityType.java @@ -9,6 +9,7 @@ import java.util.Objects; +import jakarta.persistence.Cacheable; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -19,6 +20,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; /** * Class representing an EntityType @@ -26,6 +29,8 @@ * This also has a label that will be used to identify what kind of EntityType this object is */ @Entity +@Cacheable +@Cache(usage = CacheConcurrencyStrategy.READ_ONLY) @Table(name = "entity_type") public class EntityType implements ReloadableEntity { diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipType.java b/dspace-api/src/main/java/org/dspace/content/RelationshipType.java index ba5f0531e97e..e9a84b976507 100644 --- a/dspace-api/src/main/java/org/dspace/content/RelationshipType.java +++ b/dspace-api/src/main/java/org/dspace/content/RelationshipType.java @@ -7,6 +7,7 @@ */ package org.dspace.content; +import jakarta.persistence.Cacheable; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -20,6 +21,8 @@ import jakarta.persistence.Table; import org.dspace.core.Context; import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; @@ -32,6 +35,8 @@ * The cardinality properties describe how many of each relations this relationshipType can support */ @Entity +@Cacheable +@Cache(usage = CacheConcurrencyStrategy.READ_ONLY) @Table(name = "relationship_type") public class RelationshipType implements ReloadableEntity { diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/EntityTypeDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/EntityTypeDAOImpl.java index 32af7ed35c31..8bbbbf4996f5 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/EntityTypeDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/EntityTypeDAOImpl.java @@ -53,7 +53,7 @@ public List getEntityTypesByNames(Context context, List name orderList.add(criteriaBuilder.desc(entityTypeRoot.get(EntityType_.label))); criteriaQuery.select(entityTypeRoot).orderBy(orderList); criteriaQuery.where(entityTypeRoot.get(EntityType_.LABEL).in(names)); - return list(context, criteriaQuery, false, EntityType.class, limit, offset); + return list(context, criteriaQuery, true, EntityType.class, limit, offset); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipTypeDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipTypeDAOImpl.java index 7b0e33fd41d9..0deb5afb0a5e 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipTypeDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipTypeDAOImpl.java @@ -46,7 +46,7 @@ public RelationshipType findbyTypesAndTypeName(Context context, EntityType leftT criteriaBuilder.equal(relationshipTypeRoot.get(RelationshipType_.rightType), rightType), criteriaBuilder.equal(relationshipTypeRoot.get(RelationshipType_.leftwardType), leftwardType), criteriaBuilder.equal(relationshipTypeRoot.get(RelationshipType_.rightwardType), rightwardType))); - return uniqueResult(context, criteriaQuery, false, RelationshipType.class); + return uniqueResult(context, criteriaQuery, true, RelationshipType.class); } @Override @@ -96,7 +96,7 @@ public List findByEntityType(Context context, EntityType entit List orderList = new LinkedList<>(); orderList.add(criteriaBuilder.asc(relationshipTypeRoot.get(RelationshipType_.ID))); criteriaQuery.orderBy(orderList); - return list(context, criteriaQuery, false, RelationshipType.class, limit, offset); + return list(context, criteriaQuery, true, RelationshipType.class, limit, offset); } @Override @@ -122,7 +122,7 @@ public List findByEntityType(Context context, EntityType entit criteriaBuilder.equal(relationshipTypeRoot.get(RelationshipType_.rightType), entityType) ); } - return list(context, criteriaQuery, false, RelationshipType.class, limit, offset); + return list(context, criteriaQuery, true, RelationshipType.class, limit, offset); } @Override diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql new file mode 100644 index 000000000000..2a1510ff6f04 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql @@ -0,0 +1,9 @@ +-- +-- 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/ +-- + +CREATE INDEX entity_type_label_upper_idx ON entity_type (UPPER(label)); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.05.20__entity_types_caching.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.05.20__entity_types_caching.sql new file mode 100644 index 000000000000..2a1510ff6f04 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.05.20__entity_types_caching.sql @@ -0,0 +1,9 @@ +-- +-- 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/ +-- + +CREATE INDEX entity_type_label_upper_idx ON entity_type (UPPER(label)); diff --git a/dspace/config/hibernate-ehcache-config.xml b/dspace/config/hibernate-ehcache-config.xml index e2edf67b602e..c88f03135603 100644 --- a/dspace/config/hibernate-ehcache-config.xml +++ b/dspace/config/hibernate-ehcache-config.xml @@ -145,4 +145,18 @@ + + + 500 + + + + + + 100 + + + From 554f6965501451f864b4d6aded28c89c90f6a32d Mon Sep 17 00:00:00 2001 From: abhinav Date: Wed, 21 May 2025 14:54:44 +0200 Subject: [PATCH 656/701] add expiry to ehcache-config (cherry picked from commit e6dec51c34915ddbeb83f1b09f87b41c0e777eea) --- dspace/config/hibernate-ehcache-config.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dspace/config/hibernate-ehcache-config.xml b/dspace/config/hibernate-ehcache-config.xml index c88f03135603..63c4a5ad1435 100644 --- a/dspace/config/hibernate-ehcache-config.xml +++ b/dspace/config/hibernate-ehcache-config.xml @@ -147,6 +147,9 @@ + + + 500 @@ -154,6 +157,9 @@ + + + 100 From 5bddc9a0fe01af2a0f94ab3cb6c4f4620142e498 Mon Sep 17 00:00:00 2001 From: abhinav Date: Thu, 22 May 2025 10:55:54 +0200 Subject: [PATCH 657/701] remove unsupported upper() from H2 migration (cherry picked from commit 11af1788712d3b9ec78ea2aee46bbe198efa8a65) --- .../sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql index 2a1510ff6f04..c712cfa51713 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql @@ -6,4 +6,4 @@ -- http://www.dspace.org/license/ -- -CREATE INDEX entity_type_label_upper_idx ON entity_type (UPPER(label)); +CREATE INDEX entity_type_label_upper_idx ON entity_type (label); From 8be78e0dbfd7d79f28c96633379de8b10a410e21 Mon Sep 17 00:00:00 2001 From: abhinav Date: Thu, 22 May 2025 13:13:21 +0200 Subject: [PATCH 658/701] fix cache concurrency strategy (cherry picked from commit 2091b2aa940176a4bfa5f144c72346981719bbe4) --- dspace-api/src/main/java/org/dspace/content/EntityType.java | 2 +- .../src/main/java/org/dspace/content/RelationshipType.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/EntityType.java b/dspace-api/src/main/java/org/dspace/content/EntityType.java index c46cd056eb6f..70b054c94d18 100644 --- a/dspace-api/src/main/java/org/dspace/content/EntityType.java +++ b/dspace-api/src/main/java/org/dspace/content/EntityType.java @@ -30,7 +30,7 @@ */ @Entity @Cacheable -@Cache(usage = CacheConcurrencyStrategy.READ_ONLY) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @Table(name = "entity_type") public class EntityType implements ReloadableEntity { diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipType.java b/dspace-api/src/main/java/org/dspace/content/RelationshipType.java index e9a84b976507..f00b21421e30 100644 --- a/dspace-api/src/main/java/org/dspace/content/RelationshipType.java +++ b/dspace-api/src/main/java/org/dspace/content/RelationshipType.java @@ -36,7 +36,7 @@ */ @Entity @Cacheable -@Cache(usage = CacheConcurrencyStrategy.READ_ONLY) +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) @Table(name = "relationship_type") public class RelationshipType implements ReloadableEntity { From 66b7757b80dcaa0b2f5826710c5d4040554befa3 Mon Sep 17 00:00:00 2001 From: abhinav Date: Fri, 30 May 2025 12:20:25 +0200 Subject: [PATCH 659/701] add cache expiry time (cherry picked from commit 894ffbf68704f8b89279f01bafa55db55dee6cf9) --- dspace/config/hibernate-ehcache-config.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace/config/hibernate-ehcache-config.xml b/dspace/config/hibernate-ehcache-config.xml index 63c4a5ad1435..80ba1717fe33 100644 --- a/dspace/config/hibernate-ehcache-config.xml +++ b/dspace/config/hibernate-ehcache-config.xml @@ -148,7 +148,7 @@ - + 1 500 @@ -158,7 +158,7 @@ - + 1 100 From 277e188f876740a3ea71cc69a52a0d942da8b12f Mon Sep 17 00:00:00 2001 From: abhinav Date: Mon, 2 Jun 2025 15:29:04 +0200 Subject: [PATCH 660/701] cache RelationshipDAO#findByItem as well (cherry picked from commit 8c0f5bfc566ffca00e4dc15c21de7eafe36d4e8c) --- .../java/org/dspace/content/dao/impl/RelationshipDAOImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java index 43bbc15c31b8..4717afb7d1f4 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java @@ -57,7 +57,7 @@ public List findByItem( ) ); - return list(context, criteriaQuery, false, Relationship.class, limit, offset); + return list(context, criteriaQuery, true, Relationship.class, limit, offset); } /** From 9c3b479e7eae3fd057e0edfd084e7f40b744835e Mon Sep 17 00:00:00 2001 From: abhinav Date: Wed, 25 Mar 2026 18:42:51 +0100 Subject: [PATCH 661/701] update date on migration files (cherry picked from commit e7d1e269748f322bd0672bbd2e651d5a80ec0bdd) --- ...ypes_caching.sql => V7.6_2026.03.25__entity_types_caching.sql} | 0 ...ypes_caching.sql => V7.6_2026.03.25__entity_types_caching.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/{V7.6_2025.05.20__entity_types_caching.sql => V7.6_2026.03.25__entity_types_caching.sql} (100%) rename dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/{V7.6_2025.05.20__entity_types_caching.sql => V7.6_2026.03.25__entity_types_caching.sql} (100%) diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql similarity index 100% rename from dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2025.05.20__entity_types_caching.sql rename to dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.05.20__entity_types_caching.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2026.03.25__entity_types_caching.sql similarity index 100% rename from dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2025.05.20__entity_types_caching.sql rename to dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2026.03.25__entity_types_caching.sql From 523b579f01a887e9036a8c85714f5a89198f9bd8 Mon Sep 17 00:00:00 2001 From: abhinav Date: Wed, 20 May 2026 11:27:00 +0200 Subject: [PATCH 662/701] undo cache findByItem query in RelationshipDAOImpl (cherry picked from commit 9c57f390753a369dde30c6b1f6aaab5b7fa36335) --- .../java/org/dspace/content/dao/impl/RelationshipDAOImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java index 4717afb7d1f4..43bbc15c31b8 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/RelationshipDAOImpl.java @@ -57,7 +57,7 @@ public List findByItem( ) ); - return list(context, criteriaQuery, true, Relationship.class, limit, offset); + return list(context, criteriaQuery, false, Relationship.class, limit, offset); } /** From 40333406347a312ea47306724fb492fd8c1528c6 Mon Sep 17 00:00:00 2001 From: abhinav Date: Wed, 20 May 2026 11:34:09 +0200 Subject: [PATCH 663/701] add comments to cache configs (cherry picked from commit 50737b1b24bf2207059b0300329cf065ee21c05d) --- dspace/config/hibernate-ehcache-config.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace/config/hibernate-ehcache-config.xml b/dspace/config/hibernate-ehcache-config.xml index 80ba1717fe33..680211a9acdc 100644 --- a/dspace/config/hibernate-ehcache-config.xml +++ b/dspace/config/hibernate-ehcache-config.xml @@ -145,6 +145,8 @@ + @@ -155,6 +157,7 @@ + From 5889ef83ffd4104a43a57e48907b40f35e009a72 Mon Sep 17 00:00:00 2001 From: abhinav Date: Wed, 20 May 2026 16:20:18 +0200 Subject: [PATCH 664/701] add comments to h2 migration (cherry picked from commit b49536bd8eb2b5e0209530a1ba0b78ad4e534f7c) --- .../sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql index c712cfa51713..1b6db9f52847 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2026.03.25__entity_types_caching.sql @@ -6,4 +6,6 @@ -- http://www.dspace.org/license/ -- +-- H2 does not support upper and hence the migration differs slightly from the postgres version +-- In a test environment this will not make a meaningful impact CREATE INDEX entity_type_label_upper_idx ON entity_type (label); From 0044c66495b8d88d9a880f858fe9740c0f85ba0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eike=20L=C3=B6hden?= <75250168+EikLoe@users.noreply.github.com> Date: Thu, 21 May 2026 17:28:39 +0200 Subject: [PATCH 665/701] Merge pull request #12350 from dspace-unimr/fix/ds-12349-handle-submission-with-non-public-items-and-item-filters Fix submission with non public items and AccessCondition filters (cherry picked from commit c70841f0ee92d6a655954fa53532bd47229b0207) --- .../logic/condition/ReadableByGroupCondition.java | 2 +- .../org/dspace/content/logic/LogicalFilterTest.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java index 20138beb47ef..e7b0bb7e046c 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java @@ -49,7 +49,7 @@ public boolean getResult(Context context, Item item) throws LogicalStatementExce List policies = authorizeService .getPoliciesActionFilter(context, item, Constants.getActionID(action)); for (ResourcePolicy policy : policies) { - if (policy.getGroup().getName().equals(group)) { + if (policy.getGroup() != null && policy.getGroup().getName().equals(group)) { return true; } } diff --git a/dspace-api/src/test/java/org/dspace/content/logic/LogicalFilterTest.java b/dspace-api/src/test/java/org/dspace/content/logic/LogicalFilterTest.java index 0e0864622043..c84665f29853 100644 --- a/dspace-api/src/test/java/org/dspace/content/logic/LogicalFilterTest.java +++ b/dspace-api/src/test/java/org/dspace/content/logic/LogicalFilterTest.java @@ -57,8 +57,10 @@ import org.dspace.content.service.MetadataValueService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Constants; +import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; import org.junit.After; import org.junit.Before; @@ -81,6 +83,7 @@ public class LogicalFilterTest extends AbstractUnitTest { private MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService(); private AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); private GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); + private EPersonService epersonService = EPersonServiceFactory.getInstance().getEPersonService(); // Logger private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LogicalFilterTest.class); @@ -603,6 +606,10 @@ public void testReadableByGroupCondition() { groupService.setName(g, "Test Group"); groupService.update(context, g); authorizeService.addPolicy(context, itemOne, Constants.READ, g); + EPerson e = epersonService.create(context); + epersonService.update(context, e); + authorizeService.removeAllPolicies(context, itemThree); + authorizeService.addPolicy(context, itemThree, Constants.READ, e); context.restoreAuthSystemState(); } catch (AuthorizeException | SQLException e) { fail("Exception thrown adding group READ policy to item: " + itemOne + ": " + e.getMessage()); @@ -620,6 +627,9 @@ public void testReadableByGroupCondition() { // Test the filter on itemTwo - this item has no policies: expect false assertFalse("itemTwo unexpectedly matched the 'is readable by Test Group' test", filter.getResult(context, itemTwo)); + // Test the filter on itemThree - this item has only a person related policy: expect false + assertFalse("itemThree unexpectedly matched the 'is readable by Test Group' test", + filter.getResult(context, itemThree)); } catch (LogicalStatementException e) { log.error(e.getMessage()); fail("LogicalStatementException thrown testing the ReadableByGroup filter" + e.getMessage()); From dcaaa9c892e279f9ce517788b58d4055a8cb1736 Mon Sep 17 00:00:00 2001 From: GHX5T-SOL <200635707+GHX5T-SOL@users.noreply.github.com> Date: Wed, 20 May 2026 10:47:46 +0200 Subject: [PATCH 666/701] Fix process cleanup startup timing (cherry picked from commit fb282ef7727ecfb4c6e9280810478deb301973f6) --- .../repository/ProcessRestRepository.java | 8 +++-- .../repository/ProcessRestRepositoryTest.java | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/repository/ProcessRestRepositoryTest.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessRestRepository.java index 6ec7c62aa679..9942f55b82a5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessRestRepository.java @@ -14,7 +14,6 @@ import java.util.UUID; import java.util.stream.Collectors; -import jakarta.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -38,6 +37,8 @@ import org.dspace.scripts.Process_; import org.dspace.scripts.service.ProcessService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -66,7 +67,10 @@ public class ProcessRestRepository extends DSpaceRestRepository[] { ApplicationReadyEvent.class }, eventListener.value()); + assertNull(init.getAnnotation(PostConstruct.class)); + } +} From 4a0bef4928c4cbf007592ed0fc30e7984ae9b4c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 15:50:07 +0000 Subject: [PATCH 667/701] build(deps): bump org.apache.commons:commons-configuration2 Bumps org.apache.commons:commons-configuration2 from 2.14.0 to 2.15.0. --- updated-dependencies: - dependency-name: org.apache.commons:commons-configuration2 dependency-version: 2.15.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] (cherry picked from commit 649fe8734e219d31eaa5704b4073509fa7406a2a) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0b612fe915b3..827ac0d1ee89 100644 --- a/pom.xml +++ b/pom.xml @@ -1519,7 +1519,7 @@ org.apache.commons commons-configuration2 - 2.14.0 + 2.15.0 org.apache.commons From 918ee5237832126b51e84fbb441cfa616ee78d01 Mon Sep 17 00:00:00 2001 From: Bram Maegerman Date: Wed, 1 Apr 2026 13:36:29 +0200 Subject: [PATCH 668/701] 143136: added ITs to verify bitstream get closed after retrieval (cherry picked from commit 396d4e69b6b02a626bb60e65ca00d7459f7aaca9) --- .../app/rest/BitstreamRestControllerIT.java | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java index 1e3a13f46bf5..179e93656d9e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestControllerIT.java @@ -89,6 +89,7 @@ import org.dspace.statistics.factory.StatisticsServiceFactory; import org.dspace.statistics.service.SolrLoggerService; import org.dspace.storage.bitstore.factory.StorageServiceFactory; +import org.dspace.storage.bitstore.service.BitstreamStorageService; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -1521,4 +1522,173 @@ private void givenPdf(boolean coverPageEnabled, ThrowingConsumer block) { } } + @Test + public void bitstreamInputStreamClosesWithGetRequestTest() throws Exception { + InputStream realStream = new ByteArrayInputStream("abc".getBytes()); + InputStream spyStream = Mockito.spy(realStream); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, realStream).build(); + context.restoreAuthSystemState(); + + BitstreamStorageService originalService = + StorageServiceFactory.getInstance().getBitstreamStorageService(); + BitstreamStorageService spyService = spy(originalService); + + doReturn(spyStream).when(spyService).retrieve(any(), eq(bitstream)); + + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", spyService); + + try { + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + + boolean bitstreamRetrieved = Mockito.mockingDetails(spyService) + .getInvocations().stream() + .filter(i -> i.getMethod().getName().equals("retrieve")) + .mapToInt(i -> 1) + .sum() > 0; + + if (bitstreamRetrieved) { + Mockito.verify(spyStream, times(1) + .description("InputStream should have been closed after GET request")) + .close(); + } + } finally { + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", originalService); + } + } + + @Test + public void bitstreamInputStreamClosesWithHeadRequestTest() throws Exception { + InputStream realStream = new ByteArrayInputStream("abc".getBytes()); + InputStream spyStream = Mockito.spy(realStream); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, realStream).build(); + context.restoreAuthSystemState(); + + BitstreamStorageService originalService = + StorageServiceFactory.getInstance().getBitstreamStorageService(); + BitstreamStorageService spyService = spy(originalService); + + doReturn(spyStream).when(spyService).retrieve(any(), eq(bitstream)); + + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", spyService); + + try { + getClient().perform(head("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + + boolean bitstreamRetrieved = Mockito.mockingDetails(spyService) + .getInvocations().stream() + .filter(i -> i.getMethod().getName().equals("retrieve")) + .mapToInt(i -> 1) + .sum() > 0; + + if (bitstreamRetrieved) { + Mockito.verify(spyStream, times(1) + .description("InputStream should have been closed after HEAD request")) + .close(); + } + } finally { + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", originalService); + } + } + + @Test + public void bitstreamInputStreamClosesWithGetRequestAndCitationPageEnabledTest() throws Exception { + configurationService.setProperty("citation-page.enable_globally", true); + citationDocumentService.afterPropertiesSet(); + + InputStream realStream = new ByteArrayInputStream("abc".getBytes()); + InputStream spyStream = Mockito.spy(realStream); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, realStream).build(); + context.restoreAuthSystemState(); + + BitstreamStorageService originalService = + StorageServiceFactory.getInstance().getBitstreamStorageService(); + BitstreamStorageService spyService = spy(originalService); + + doReturn(spyStream).when(spyService).retrieve(any(), eq(bitstream)); + + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", spyService); + + try { + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + + boolean bitstreamRetrieved = Mockito.mockingDetails(spyService) + .getInvocations().stream() + .filter(i -> i.getMethod().getName().equals("retrieve")) + .mapToInt(i -> 1) + .sum() > 0; + + if (bitstreamRetrieved) { + Mockito.verify(spyStream, times(1) + .description("InputStream should have been closed after GET request")) + .close(); + } + } finally { + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", originalService); + } + } + + @Test + public void bitstreamInputStreamClosesWithHeadRequestAndCitationPageEnabledTest() throws Exception { + configurationService.setProperty("citation-page.enable_globally", true); + citationDocumentService.afterPropertiesSet(); + + InputStream realStream = new ByteArrayInputStream("abc".getBytes()); + InputStream spyStream = Mockito.spy(realStream); + + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + + Bitstream bitstream = BitstreamBuilder.createBitstream(context, item, realStream).build(); + context.restoreAuthSystemState(); + + BitstreamStorageService originalService = + StorageServiceFactory.getInstance().getBitstreamStorageService(); + BitstreamStorageService spyService = spy(originalService); + + doReturn(spyStream).when(spyService).retrieve(any(), eq(bitstream)); + + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", spyService); + + try { + getClient().perform(head("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + + boolean bitstreamRetrieved = Mockito.mockingDetails(spyService) + .getInvocations().stream() + .filter(i -> i.getMethod().getName().equals("retrieve")) + .mapToInt(i -> 1) + .sum() > 0; + + if (bitstreamRetrieved) { + Mockito.verify(spyStream, times(1) + .description("InputStream should have been closed after HEAD request")) + .close(); + } + } finally { + ReflectionTestUtils.setField(bitstreamService, "bitstreamStorageService", originalService); + } + } } From 91d86b59df7d0367bf1873259467ac007ccc9024 Mon Sep 17 00:00:00 2001 From: Bram Maegerman Date: Wed, 1 Apr 2026 14:44:53 +0200 Subject: [PATCH 669/701] 143136: fixed bitstream InputStreams not getting closed in several classes (cherry picked from commit e2915c9e6604e09be741afe908a231344ecce712) --- .../requestitem/RequestItemEmailNotifier.java | 15 ++++- .../app/rest/utils/BitstreamResource.java | 65 +++++++++++++++++-- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java index c489fb4b3ff0..49b82210dc23 100644 --- a/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java +++ b/dspace-api/src/main/java/org/dspace/app/requestitem/RequestItemEmailNotifier.java @@ -9,7 +9,9 @@ package org.dspace.app.requestitem; import java.io.IOException; +import java.io.InputStream; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import jakarta.annotation.ManagedBean; @@ -186,6 +188,7 @@ public void sendResponse(Context context, RequestItem ri, String subject, email.setSubject(subject); email.addRecipient(ri.getReqEmail()); // Attach bitstreams. + List bitstreamInputStreams = new ArrayList<>(); try { if (ri.isAccept_request()) { if (ri.isAllfiles()) { @@ -200,11 +203,13 @@ public void sendResponse(Context context, RequestItem ri, String subject, // #8636 Anyone receiving the email can respond to the // request without authenticating into DSpace context.turnOffAuthorisationSystem(); + InputStream is = bitstreamService.retrieve(context, bitstream); email.addAttachment( - bitstreamService.retrieve(context, bitstream), + is, bitstream.getName(), bitstream.getFormat(context).getMIMEType()); context.restoreAuthSystemState(); + bitstreamInputStreams.add(is); } } } @@ -212,10 +217,12 @@ public void sendResponse(Context context, RequestItem ri, String subject, Bitstream bitstream = ri.getBitstream(); // #8636 Anyone receiving the email can respond to the request without authenticating into DSpace context.turnOffAuthorisationSystem(); - email.addAttachment(bitstreamService.retrieve(context, bitstream), + InputStream is = bitstreamService.retrieve(context, bitstream); + email.addAttachment(is, bitstream.getName(), bitstream.getFormat(context).getMIMEType()); context.restoreAuthSystemState(); + bitstreamInputStreams.add(is); } email.send(); } else { @@ -231,6 +238,10 @@ public void sendResponse(Context context, RequestItem ri, String subject, LOG.warn(LogHelper.getHeader(context, "error_mailing_requestItem", e.getMessage())); throw new IOException("Reply not sent: " + e.getMessage()); + } finally { + for (InputStream bitstreamInputStream : bitstreamInputStreams) { + bitstreamInputStream.close(); + } } LOG.info(LogHelper.getHeader(context, "sent_attach_requestItem", "token={}"), ri.getToken()); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java index 1ac6a320d9c2..1f2219894f87 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java @@ -92,7 +92,7 @@ public String getDescription() { public InputStream getInputStream() throws IOException { fetchDocument(); - return document.inputStream(); + return document.getInputstream(); } @Override @@ -110,7 +110,7 @@ public long contentLength() { public String getChecksum() { fetchDocument(); - return document.etag(); + return document.getEtag(); } private void fetchDocument() { @@ -123,13 +123,13 @@ private void fetchDocument() { if (shouldGenerateCoverPage) { var coverPage = getCoverpageByteArray(context, bitstream); - this.document = new BitstreamDocument(etag(bitstream), + this.document = new BitstreamDocumentCoverPage(etag(bitstream), coverPage.length, new ByteArrayInputStream(coverPage)); } else { - this.document = new BitstreamDocument(bitstream.getChecksum(), + this.document = new BitstreamDocumentInputstream(bitstream.getChecksum(), bitstream.getSizeBytes(), - bitstreamService.retrieve(context, bitstream)); + bitstream.getID()); } } catch (SQLException | AuthorizeException | IOException e) { throw new RuntimeException(e); @@ -158,12 +158,63 @@ However it looks like the coverpage generation is not stable (e.g. if invoked tw } private Context initializeContext() throws SQLException { - Context context = new Context(); + Context context = new Context(Context.Mode.READ_ONLY); EPerson currentUser = ePersonService.find(context, currentUserUUID); context.setCurrentUser(currentUser); currentSpecialGroups.forEach(context::setSpecialGroup); return context; } - private record BitstreamDocument(String etag, long length, InputStream inputStream) {} + protected abstract class BitstreamDocument { + private final String etag; + private final long length; + + protected BitstreamDocument(String etag, long length) { + this.etag = etag; + this.length = length; + } + + abstract InputStream getInputstream(); + + public String getEtag() { + return etag; + } + + public long length() { + return length; + } + } + + protected class BitstreamDocumentInputstream extends BitstreamDocument { + protected final UUID bitstreamUUID; + + public BitstreamDocumentInputstream(String etag, long length, UUID bitstreamUUID) { + super(etag, length); + this.bitstreamUUID = bitstreamUUID; + } + + @Override + public InputStream getInputstream() { + try (Context context = initializeContext()) { + return bitstreamService.retrieve(context, bitstreamService.find(context, bitstreamUUID)); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(e); + } + } + } + + protected class BitstreamDocumentCoverPage extends BitstreamDocument { + private final ByteArrayInputStream coverpage; + + public BitstreamDocumentCoverPage(String etag, long length, ByteArrayInputStream byteArrayInputStream) { + super(etag, length); + this.coverpage = byteArrayInputStream; + } + + @Override + public InputStream getInputstream() { + return coverpage; + } + } + } From 8a7cbfa64e4eec12a00b0074d36cb709a0364ba0 Mon Sep 17 00:00:00 2001 From: Bram Maegerman Date: Thu, 21 May 2026 08:48:08 +0200 Subject: [PATCH 670/701] 140136: implemented InputStreamSource on BitstreamDocument (cherry picked from commit c5aa562ccf1ecf117b559d5c8d0a6fabbe534650) --- .../org/dspace/app/rest/utils/BitstreamResource.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java index 1f2219894f87..3d10d9d02c10 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/BitstreamResource.java @@ -28,6 +28,7 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.utils.DSpace; import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.InputStreamSource; import org.springframework.util.DigestUtils; /** @@ -92,7 +93,7 @@ public String getDescription() { public InputStream getInputStream() throws IOException { fetchDocument(); - return document.getInputstream(); + return document.getInputStream(); } @Override @@ -165,7 +166,7 @@ private Context initializeContext() throws SQLException { return context; } - protected abstract class BitstreamDocument { + protected abstract class BitstreamDocument implements InputStreamSource { private final String etag; private final long length; @@ -174,8 +175,6 @@ protected BitstreamDocument(String etag, long length) { this.length = length; } - abstract InputStream getInputstream(); - public String getEtag() { return etag; } @@ -194,7 +193,7 @@ public BitstreamDocumentInputstream(String etag, long length, UUID bitstreamUUID } @Override - public InputStream getInputstream() { + public InputStream getInputStream() throws IOException { try (Context context = initializeContext()) { return bitstreamService.retrieve(context, bitstreamService.find(context, bitstreamUUID)); } catch (SQLException | AuthorizeException | IOException e) { @@ -212,7 +211,7 @@ public BitstreamDocumentCoverPage(String etag, long length, ByteArrayInputStream } @Override - public InputStream getInputstream() { + public InputStream getInputStream() { return coverpage; } } From a9d8c09a5a6dfb8ca46550909ac2478b1b008f51 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 16 May 2026 22:05:27 +0200 Subject: [PATCH 671/701] ORE aggregated resource URI validation --- .../crosswalk/OREIngestionCrosswalk.java | 84 +++++++++++++++++-- dspace/config/modules/oai.cfg | 7 ++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java index f756aae22577..9e890a6046fa 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/OREIngestionCrosswalk.java @@ -11,6 +11,8 @@ import java.io.IOException; import java.io.InputStream; import java.net.ConnectException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.sql.SQLException; import java.text.NumberFormat; @@ -18,6 +20,8 @@ import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.Set; import org.apache.logging.log4j.Logger; @@ -34,6 +38,8 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; @@ -76,6 +82,7 @@ public class OREIngestionCrosswalk .getBitstreamFormatService(); protected BundleService bundleService = ContentServiceFactory.getInstance().getBundleService(); protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @Override @@ -173,9 +180,13 @@ public void ingest(Context context, DSpaceObject dso, Element root, boolean crea try { // Make sure the url string escapes all the oddball characters String processedURL = encodeForURL(href); - // Generate a requeset for the aggregated resource - ARurl = new URL(processedURL); - in = ARurl.openStream(); + if (validResourceUri(entryId, processedURL)) { + // Generate a request for the aggregated resource + ARurl = new URL(processedURL); + in = ARurl.openStream(); + } else { + throw new FileNotFoundException("Failed to validate " + processedURL); + } } catch (FileNotFoundException fe) { log.error("The provided URI failed to return a resource: " + href); } catch (ConnectException fe) { @@ -219,17 +230,17 @@ public void ingest(Context context, DSpaceObject dso, Element root, boolean crea * @param sourceString source unescaped string */ private String encodeForURL(String sourceString) { - Character lowalpha[] = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', + Character[] lowalpha = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; - Character upalpha[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', + Character[] upalpha = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; - Character digit[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; - Character mark[] = {'-', '_', '.', '!', '~', '*', '\'', '(', ')'}; + Character[] digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + Character[] mark = {'-', '_', '.', '!', '~', '*', '\'', '(', ')'}; // reserved - Character reserved[] = {';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '%', '#'}; + Character[] reserved = {';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '%', '#'}; Set URLcharsSet = new HashSet(); URLcharsSet.addAll(Arrays.asList(lowalpha)); @@ -251,4 +262,61 @@ private String encodeForURL(String sourceString) { return processedString.toString(); } + /** + * Validate a resource URI against the host and scheme of the remote OAI endpoint, or a configured + * list of allowed prefixes. + * This still implicitly "trusts" the remote OAI server, but will reject resource URIs with a totally + * different hostname to avoid downloading malicious resources from a compromised endpoint. + * Even if the URL prefix validation is disabled, schemes will still be enforced to http(s) so file:/// and + * other unwanted schemes cannot be used + * @param entryUrl the entryId of the parent ORE resource + * @param resourceUrl the resource URL of the aggregated ORE resource + * @return result of the validation + */ + private boolean validResourceUri(String entryUrl, String resourceUrl) { + try { + Set allowedSchemes = Set.of("http", "https"); + URI entryUri = new URI(entryUrl).normalize(); + URI resourceUri = new URI(resourceUrl).normalize(); + String scheme = resourceUri.getScheme(); + + if (scheme == null || + !allowedSchemes.contains(scheme.toLowerCase(Locale.ROOT))) { + log.warn("Illegal scheme requested for ORE resource: {}", resourceUri); + return false; + } + + if (configurationService.getBooleanProperty("oai.harvester.ore.file.validateUrlPrefix", false)) { + for (String allowedPrefix : configurationService + .getArrayProperty("oai.harvester.ore.file.allowedUrlPrefix")) { + URI allowedUri = new URI(allowedPrefix).normalize(); + // Return true on the first allowed prefix match + if (Objects.equals(resourceUri.getScheme(), allowedUri.getScheme()) + && Objects.equals(resourceUri.getHost().toLowerCase(Locale.ROOT), + allowedUri.getHost().toLowerCase(Locale.ROOT))) { + return true; + } + } + + // If no allowed prefixes were matched, we require scheme + host to match the remote OAI server + if (!Objects.equals(entryUri.getScheme(), resourceUri.getScheme())) { + log.warn("Illegal scheme requested for ORE resource: {}", resourceUri); + return false; + } + if (!Objects.equals( + entryUri.getHost().toLowerCase(Locale.ROOT), + resourceUri.getHost().toLowerCase(Locale.ROOT))) { + log.warn("Illegal host requested for ORE resource: {}", resourceUri); + return false; + } + } + + return true; + + } catch (URISyntaxException e) { + log.warn("Could not validate ORE resource URI: {}", resourceUrl); + return false; + } + } + } diff --git a/dspace/config/modules/oai.cfg b/dspace/config/modules/oai.cfg index 376fd6aec190..1d36cce175d2 100644 --- a/dspace/config/modules/oai.cfg +++ b/dspace/config/modules/oai.cfg @@ -145,3 +145,10 @@ oai.harvester.unknownSchema = fail # when attempting to find the handle of harvested items. If there is a match with # this config parameter, a new handle will be minted instead. Default value: 123456789. #oai.harvester.rejectedHandlePrefix = 123456789, myTestHandle + +# If ingesting files with ORE, only files with URLs that match the base URL of the remote +# OAI endpoint's domain name are accepted, or a list of other URL prefixes defined below +#oai.harvester.ore.file.validateUrlPrefix = true +# Prefixes that are allowed globally (for any endpoint) are below +#oai.harvester.ore.file.allowedUrlPrefix = dspace.myinstitution.edu +#oai.harvester.ore.file.allowedUrlPrefix = files.myinstitution.edu From add595a98fdaf3f1fafa0012e8cb3d58d70a5d4e Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 16 May 2026 20:54:09 +0200 Subject: [PATCH 672/701] Filter requests for JSPs or traversal (cherry picked from commit cf9be8554d3597e2c80958cd62336b40b79ba19d) --- .../security/GlobalRequestSecurityFilter.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java new file mode 100644 index 000000000000..53d6f7065333 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java @@ -0,0 +1,134 @@ +/** + * 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.app.rest.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +/** + * Global filter acting on all requests (not just /api/) to provide some additional hardening + * against common attacks or RCE, if a malicious payload was somehow written to a directory + * executable by the servlet container. + * The decoding and normalisation is designed to be tolerant of malformed URLs or broken clients, etc. + * so that this additional security filter does not introduce false positives or unintended side effects. + * + * @author Kim Shepherd + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class GlobalRequestSecurityFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String normalizedPath = normaliseUrl(request.getRequestURI()); + // Return 403 forbidden if JSP execution or URL traversal is attempted + if (isTraversalAttempt(normalizedPath) || isJspExecutionAttempt(normalizedPath)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + filterChain.doFilter(request, response); + } + + /** + * Normalise the URI similarly to Tomcat, for testing how it will be interpreted + * @param rawUrl the unvalidated URL string + * @return a decoded, normalise URL + */ + private String normaliseUrl(String rawUrl) throws IOException { + if (rawUrl == null || rawUrl.isBlank()) { + throw new IOException("Empty URL"); + } + String url = rawUrl.split("\\?")[0]; + // Strip ;jspsession=... and so on + int semicolon = url.indexOf(';'); + if (semicolon >= 0) { + url = url.substring(0, semicolon); + } + url = decodeUrl(url); + if (url == null || url.isBlank()) { + throw new IOException("Decoded URL path is empty"); + } + url = normaliseUrlPath(url); + if (url == null || url.isBlank()) { + throw new IOException("Normalised URL path is empty"); + } + return url.toLowerCase(Locale.ROOT); + } + + /** + * Decode URL, falling back to original URL if it's malformed or undecodable + * @param url the encoded / unvalidated URL + * @return decoded URL or the original URL on error + */ + private String decodeUrl(String url) { + try { + return URLDecoder.decode(url, StandardCharsets.UTF_8); + } catch (IllegalArgumentException ex) { + // if we can't decode it, just return raw string + return url; + } + } + + /** + * Normalise the URL path and ensure it ends in a / + * @param url the URL path to normalise + * @return normalised path or the original parameter on error + */ + private String normaliseUrlPath(String url) { + try { + if (!url.startsWith("/")) { + url = "/" + url; + } + return new URI(url).normalize().getPath(); + } catch (Exception e) { + // if we can't use or normalise the path, just return the raw string + return url; + } + } + + /** + * Detect traversal after normalisation + * @param url the URL path to validate + * @return true if this looks like a traversal attempt + */ + private boolean isTraversalAttempt(String url) { + return url.contains("../") + || url.contains("/..") + || url.contains("%2e%2e") + || url.contains(".."); + } + + /** + * Block JSP execution attempts + * @param url the URL path to validate + */ + private boolean isJspExecutionAttempt(String url) { + return url.endsWith(".jsp") + || url.endsWith(".jspx") + || url.contains(".jsp/") + || url.contains(".jspx/") + || url.contains(".jsp\0") + || url.contains(".jspx\0"); + } +} \ No newline at end of file From 647b6c21a6b5288fc64a7c07a26f870dda1a0e0a Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 16 May 2026 22:18:27 +0200 Subject: [PATCH 673/701] Add additional logging to GlobalRequestSecurityFilter (cherry picked from commit 295a046fba14502619b3e3d96a7f6abdc9a4a5fc) --- .../app/rest/security/GlobalRequestSecurityFilter.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java index 53d6f7065333..13740a53c7a4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java @@ -43,7 +43,13 @@ protected void doFilterInternal( ) throws ServletException, IOException { String normalizedPath = normaliseUrl(request.getRequestURI()); // Return 403 forbidden if JSP execution or URL traversal is attempted - if (isTraversalAttempt(normalizedPath) || isJspExecutionAttempt(normalizedPath)) { + if (isTraversalAttempt(normalizedPath)) { + logger.warn("Path traversal attempt detected. Skipping request: " + request.getRequestURI()); + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return; + } + if (isJspExecutionAttempt(normalizedPath)) { + logger.warn("JSP execution attempt detected. Skipping request: " + request.getRequestURI()); response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } From f979ce65818ac491550971a8e1e6fd4824a3a3a3 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 11:00:17 +0200 Subject: [PATCH 674/701] Fix import order (cherry picked from commit e2e6a796fd8d19de18a80b735e20a62d29c3c5cd) --- .../rest/security/GlobalRequestSecurityFilter.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java index 13740a53c7a4..78e433a8fad9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GlobalRequestSecurityFilter.java @@ -7,6 +7,12 @@ */ package org.dspace.app.rest.security; +import java.io.IOException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -16,12 +22,6 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; -import java.io.IOException; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.Locale; - /** * Global filter acting on all requests (not just /api/) to provide some additional hardening * against common attacks or RCE, if a malicious payload was somehow written to a directory @@ -137,4 +137,4 @@ private boolean isJspExecutionAttempt(String url) { || url.contains(".jsp\0") || url.contains(".jspx\0"); } -} \ No newline at end of file +} From 52e5daba5820fc20577f324335f836c18b82738b Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 13:38:55 +0200 Subject: [PATCH 675/701] Update sitemap traversal test expectations (cherry picked from commit 56ae2871eaba764900e4d9e23685a9472f485069) --- .../java/org/dspace/app/rest/SitemapRestControllerIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java index 681e9bf16b04..084b8272bd09 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java @@ -134,7 +134,7 @@ public void testSitemap_fileSystemTraversal_dspaceCfg() throws Exception { //** WHEN ** //We attempt to use endpoint for malicious file system traversal getClient().perform(get("/" + SITEMAPS_ENDPOINT + "/%2e%2e/config/dspace.cfg")) - .andExpect(status().isBadRequest()); + .andExpect(status().isForbidden()); } @Test @@ -142,7 +142,7 @@ public void testSitemap_fileSystemTraversal_dspaceCfg2() throws Exception { //** WHEN ** //We attempt to use endpoint for malicious file system traversal getClient().perform(get("/" + SITEMAPS_ENDPOINT + "/%2e%2e%2fconfig%2fdspace.cfg")) - .andExpect(status().isBadRequest()); + .andExpect(status().isForbidden()); } @Test From e56a5abb4f32781e49e77226ebad3e010a105e10 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 5 May 2026 16:24:29 +0200 Subject: [PATCH 676/701] Velocity and template safety for Email and LDN messages * Safer Velocity configuration * New "message.templates.allowed-config" config * Remove "UnmodifiableConfiguration" in favour of a simple Map of whitelisted Config keys/values * Centralise Velocity config in core Utils * Small javadoc changes (cherry picked from commit b2d6141389f5652970b366325ed9deff21a86836) --- .../src/main/java/org/dspace/core/Email.java | 81 +++++-------------- .../src/main/java/org/dspace/core/LDN.java | 53 +----------- .../src/main/java/org/dspace/core/Utils.java | 64 +++++++++++++++ dspace/config/dspace.cfg | 12 +++ 4 files changed, 99 insertions(+), 111 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Email.java b/dspace-api/src/main/java/org/dspace/core/Email.java index 74a48b3d82c9..891e49ea4d36 100644 --- a/dspace-api/src/main/java/org/dspace/core/Email.java +++ b/dspace-api/src/main/java/org/dspace/core/Email.java @@ -22,7 +22,6 @@ import java.util.Date; import java.util.Enumeration; import java.util.List; -import java.util.Properties; import jakarta.activation.DataHandler; import jakarta.activation.DataSource; @@ -44,18 +43,16 @@ import org.apache.logging.log4j.Logger; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.Velocity; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.MethodInvocationException; import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; -import org.apache.velocity.runtime.resource.loader.StringResourceLoader; import org.apache.velocity.runtime.resource.util.StringResourceRepository; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; /** - * Builder representing an e-mail message. The {@link send} method causes the + * Builder representing an e-mail message. The {@link #send} method causes the * assembled message to be formatted and sent. *

* Typical use: @@ -72,7 +69,7 @@ * Apache Velocity. They may contain VTL directives and property * placeholders. *

- * {@link addArgument(string)} adds a property to the {@code params} array + * {@link #addArgument(Object)} adds a property to the {@code params} array * in the Velocity context, which can be used to replace placeholder tokens * in the message. These arguments are indexed by number in the order they were * added to the message. @@ -80,9 +77,9 @@ * The DSpace configuration properties are also available to templates as the * array {@code config}, indexed by name. Example: {@code ${config.get('dspace.name')}} *

- * Recipients and attachments may be added as needed. See {@link addRecipient}, - * {@link addAttachment(File, String)}, and - * {@link addAttachment(InputStream, String, String)}. + * Recipients and attachments may be added as needed. See {@link #addRecipient}, + * {@link #addAttachment(File, String)}, and + * {@link #addAttachment(InputStream, String, String)}. *

* Headers such as Subject may be supplied by the template, by defining them * using the VTL directive {@code #set()}. Only headers named in the DSpace @@ -125,8 +122,8 @@ * *

* There are two ways to load a message body. One can create an instance of - * {@link Email} and call {@link setContent} on it, passing the body as a String. Or - * one can use the static factory method {@link getEmail} to load a file by its + * {@link Email} and call {@link #setContent} on it, passing the body as a String. Or + * one can use the static factory method {@link #getEmail} to load a file by its * complete filesystem path. In either case the text will be loaded into a * Velocity template. * @@ -173,20 +170,11 @@ public class Email { private static final Logger LOG = LogManager.getLogger(); + private static final ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + /** Velocity template settings. */ private static final String RESOURCE_REPOSITORY_NAME = "Email"; - private static final Properties VELOCITY_PROPERTIES = new Properties(); - static { - VELOCITY_PROPERTIES.put(Velocity.RESOURCE_LOADERS, "string"); - VELOCITY_PROPERTIES.put("resource.loader.string.description", - "Velocity StringResource loader"); - VELOCITY_PROPERTIES.put("resource.loader.string.class", - StringResourceLoader.class.getName()); - VELOCITY_PROPERTIES.put("resource.loader.string.repository.name", - RESOURCE_REPOSITORY_NAME); - VELOCITY_PROPERTIES.put("resource.loader.string.repository.static", - "false"); - } /** Velocity template for a message body */ private Template template; @@ -230,7 +218,7 @@ public void setContent(String name, String content) { arguments.clear(); VelocityEngine templateEngine = new VelocityEngine(); - templateEngine.init(VELOCITY_PROPERTIES); + templateEngine.init(Utils.getSecureVelocityProperties(RESOURCE_REPOSITORY_NAME)); StringResourceRepository repo = (StringResourceRepository) templateEngine.getApplicationAttribute(RESOURCE_REPOSITORY_NAME); @@ -348,10 +336,7 @@ public void reset() { public void send() throws MessagingException, IOException { build(); - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - boolean disabled = config.getBooleanProperty("mail.server.disabled", false); - if (disabled) { + if (isMailServerDisabled(configurationService)) { LOG.info(format(message, body)); } else { Transport.send(message); @@ -364,7 +349,7 @@ public void send() throws MessagingException, IOException { * {@code mail.message.headers} then that name and its value will be added * to the message's headers. * - *

"subject" is treated specially: if {@link setSubject()} has not been + *

"subject" is treated specially: if {@link #setSubject} has not been * called, the value of any "subject" property will be used as if setSubject * had been called with that value. Thus a template may define its subject, * but the caller may override it. @@ -379,15 +364,12 @@ void build() throw new MessagingException("Email has no body"); } - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - // Get the mail configuration properties - String from = config.getProperty("mail.from.address"); + String from = configurationService.getProperty("mail.from.address"); // If no character set specified, attempt to retrieve a default if (charset == null) { - charset = config.getProperty("mail.charset"); + charset = configurationService.getProperty("mail.charset"); } // Get session @@ -402,11 +384,13 @@ void build() new InternetAddress(recipient)); } // Get headers defined by the template. - String[] templateHeaders = config.getArrayProperty("mail.message.headers"); + String[] templateHeaders = configurationService.getArrayProperty("mail.message.headers"); // Format the mail message body VelocityContext vctx = new VelocityContext(); - vctx.put("config", new UnmodifiableConfigurationService(config)); + // Pass a restricted (via configuration) list of resolved Configuration keys and values, for + // template lookup + vctx.put("config", Utils.getAllowedTemplateConfig()); vctx.put("params", Collections.unmodifiableList(arguments)); StringWriter writer = new StringWriter(); @@ -709,31 +693,4 @@ public OutputStream getOutputStream() throws IOException { throw new IOException("Cannot write to this read-only resource"); } } - - /** - * Wrap ConfigurationService to prevent templates from modifying - * the configuration. - */ - public static class UnmodifiableConfigurationService { - private final ConfigurationService configurationService; - - /** - * Swallow an instance of ConfigurationService. - * - * @param cs the real instance, to be wrapped. - */ - public UnmodifiableConfigurationService(ConfigurationService cs) { - configurationService = cs; - } - - /** - * Look up a key in the actual ConfigurationService. - * - * @param key to be looked up in the DSpace configuration. - * @return whatever value ConfigurationService associates with {@code key}. - */ - public String get(String key) { - return configurationService.getProperty(key); - } - } } diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java index 8ae5cddf5b4a..c0c5690817cf 100644 --- a/dspace-api/src/main/java/org/dspace/core/LDN.java +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Properties; import jakarta.mail.MessagingException; import org.apache.commons.lang3.StringUtils; @@ -26,15 +25,11 @@ import org.apache.logging.log4j.Logger; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.Velocity; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.MethodInvocationException; import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; -import org.apache.velocity.runtime.resource.loader.StringResourceLoader; import org.apache.velocity.runtime.resource.util.StringResourceRepository; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; /** * Class representing an LDN message json @@ -57,18 +52,6 @@ public class LDN { /** Velocity template settings. */ private static final String RESOURCE_REPOSITORY_NAME = "LDN"; - private static final Properties VELOCITY_PROPERTIES = new Properties(); - static { - VELOCITY_PROPERTIES.put(Velocity.RESOURCE_LOADERS, "string"); - VELOCITY_PROPERTIES.put("resource.loader.string.description", - "Velocity StringResource loader"); - VELOCITY_PROPERTIES.put("resource.loader.string.class", - StringResourceLoader.class.getName()); - VELOCITY_PROPERTIES.put("resource.loader.string.repository.name", - RESOURCE_REPOSITORY_NAME); - VELOCITY_PROPERTIES.put("resource.loader.string.repository.static", - "false"); - } /** Velocity template for the message*/ private Template template; @@ -112,14 +95,13 @@ public void addArgument(Object arg) { * @throws IOException if IO error */ public String generateLDNMessage() { - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - VelocityEngine templateEngine = new VelocityEngine(); - templateEngine.init(VELOCITY_PROPERTIES); + templateEngine.init(Utils.getSecureVelocityProperties(RESOURCE_REPOSITORY_NAME)); VelocityContext vctx = new VelocityContext(); - vctx.put("config", new LDN.UnmodifiableConfigurationService(config)); + // Pass a restricted (via configuration) list of resolved Configuration keys and values, for + // template lookup + vctx.put("config", Utils.getAllowedTemplateConfig()); vctx.put("params", Collections.unmodifiableList(arguments)); if (null == template) { @@ -182,31 +164,4 @@ public static LDN getLDNMessage(String ldnMessageFile) ldn.setContent(ldnMessageFile, contentBuffer.toString()); return ldn; } - - /** - * Wrap ConfigurationService to prevent templates from modifying - * the configuration. - */ - public static class UnmodifiableConfigurationService { - private final ConfigurationService configurationService; - - /** - * Swallow an instance of ConfigurationService. - * - * @param cs the real instance, to be wrapped. - */ - public UnmodifiableConfigurationService(ConfigurationService cs) { - configurationService = cs; - } - - /** - * Look up a key in the actual ConfigurationService. - * - * @param key to be looked up in the DSpace configuration. - * @return whatever value ConfigurationService associates with {@code key}. - */ - public String get(String key) { - return configurationService.getProperty(key); - } - } } diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index a1294c3317ce..045a5c76cd36 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -29,16 +29,22 @@ import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; +import java.util.List; +import java.util.Map; +import java.util.Properties; import java.util.Random; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import com.coverity.security.Escape; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringSubstitutor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.runtime.resource.loader.StringResourceLoader; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -101,6 +107,14 @@ public final class Utils { private static final Calendar outCal = GregorianCalendar.getInstance(); + // Allowed configuration properties to pass to Velocity templates (Email, LDN) + private static final String[] DEFAULT_ALLOWED_TEMPLATE_CONFIGS = { + "dspace.name", "dspace.shortname", "dspace.ui.url", + "mail.helpdesk", "mail.message.helpdesk.telephone", "mail.admin", "mail.admin.name"}; + + private static final ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + /** * Private constructor */ @@ -507,4 +521,54 @@ public static String interpolateConfigsInString(String string) { return StringSubstitutor.replace(string, config.getProperties()); } + /** + * Get a list of allowed DSpace configuration property keys that will be exposed to Velocity templates + * (used in Email and LDN messages) as a simple Map of strings. + * @return Map of strings representing resolved configuration properties + */ + public static Map getAllowedTemplateConfig() { + // Pass a restricted (via configuration) list of resolved Configuration keys and values, for + // template lookup + List allowedConfigurationKeys = List.of(configurationService.getArrayProperty( + "message.templates.allowed-config", DEFAULT_ALLOWED_TEMPLATE_CONFIGS)); + return allowedConfigurationKeys.stream() + .map(key -> Map.entry(key, configurationService.getProperty(key))) + .filter(entry -> entry.getValue() != null) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + } + + /** + * Create and return a set of default, secure Velocity configuration properties. + * @see {@link LDN}, {@link Email} + * + * @param resourceRepositoryName the templating context e.g. "LDN", "Email" + * @returns secure Velocity configuration for use with templating + */ + public static Properties getSecureVelocityProperties(String resourceRepositoryName) { + Properties secureVelocityProperties = new Properties(); + // Basic Velocity configuration + secureVelocityProperties.setProperty(Velocity.RESOURCE_LOADERS, "string"); + secureVelocityProperties.setProperty("resource.loader.string.description", + "Velocity StringResource loader"); + secureVelocityProperties.setProperty("resource.loader.string.class", + StringResourceLoader.class.getName()); + secureVelocityProperties.setProperty("resource.loader.string.repository.name", + resourceRepositoryName); + secureVelocityProperties.setProperty("resource.loader.string.repository.static", + "false"); + // Set secure default introspection and class restriction handling in Velocity + secureVelocityProperties.setProperty("introspector.uberspect.class", + "org.apache.velocity.util.introspection.SecureUberspector"); + secureVelocityProperties.setProperty("introspector.restrict.classes", + "java.lang.Class,java.lang.Runtime,java.lang.System"); + secureVelocityProperties.setProperty( "introspector.restrict.packages", + "java.lang.reflect,java.io,java.nio"); + secureVelocityProperties.setProperty("runtime.strict_mode.enable", "true"); + + return secureVelocityProperties; + } + } diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d25a935260dc..cdd11e95c504 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -162,6 +162,7 @@ mail.from.address = dspace-noreply@myu.edu # will use the above settings to create a Session. #mail.session.name = Session + # When feedback is submitted via the Feedback form, it is sent to this address # Currently limited to one recipient! # if this property is empty or commented out, feedback form is disabled @@ -228,6 +229,17 @@ mail.message.headers = charset # Helpdesk telephone. Not email, but should be with other contact info. Optional. #mail.message.helpdesk.telephone = +1 555 555 5555 +# Allowed configuration properties, to pass in a "config" map to email and LDN templates. +# This allows templates to easily access dynamic configuration properties, without +# exposing sensitive information to the templating engine +message.templates.allowed-config = dspace.name +message.templates.allowed-config = dspace.shortname +message.templates.allowed-config = dspace.ui.url +message.templates.allowed-config = mail.helpdesk +message.templates.allowed-config = mail.message.helpdesk.telephone +message.templates.allowed-config = mail.admin +message.templates.allowed-config = mail.admin.name + ##### Asset Storage (bitstreams / files) ###### # Moved to config/spring/api/bitstore.xml From 2d167bd8e0ddff573a6ae9966bb6b1700f792bef Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 15:28:25 +0200 Subject: [PATCH 677/701] Backport fixes for 8.x: Emails --- dspace-api/src/main/java/org/dspace/core/Email.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Email.java b/dspace-api/src/main/java/org/dspace/core/Email.java index 891e49ea4d36..da2a7403fbaa 100644 --- a/dspace-api/src/main/java/org/dspace/core/Email.java +++ b/dspace-api/src/main/java/org/dspace/core/Email.java @@ -336,7 +336,10 @@ public void reset() { public void send() throws MessagingException, IOException { build(); - if (isMailServerDisabled(configurationService)) { + ConfigurationService config + = DSpaceServicesFactory.getInstance().getConfigurationService(); + boolean disabled = config.getBooleanProperty("mail.server.disabled", false); + if (disabled) { LOG.info(format(message, body)); } else { Transport.send(message); From 94bd867de0ff2e16a73f126a880115a63d26304c Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 16 May 2026 15:55:24 +0200 Subject: [PATCH 678/701] Better null checking in allowed config props (cherry picked from commit 6b665313cb48131ada04ae0840ff531b08b31dad) --- dspace-api/src/main/java/org/dspace/core/Utils.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index 045a5c76cd36..b90e0d0fc306 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -31,6 +31,7 @@ import java.util.GregorianCalendar; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Properties; import java.util.Random; import java.util.StringTokenizer; @@ -532,8 +533,11 @@ public static Map getAllowedTemplateConfig() { List allowedConfigurationKeys = List.of(configurationService.getArrayProperty( "message.templates.allowed-config", DEFAULT_ALLOWED_TEMPLATE_CONFIGS)); return allowedConfigurationKeys.stream() - .map(key -> Map.entry(key, configurationService.getProperty(key))) - .filter(entry -> entry.getValue() != null) + .map(key -> { + String value = configurationService.getProperty(key); + return value != null ? Map.entry(key, value) : null; + }) + .filter(Objects::nonNull) .collect(Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue From 34bdb6459c48a3d055b824f2bb32285c0b622327 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 09:57:21 +0200 Subject: [PATCH 679/701] Access configurationService at runtime, not rely on class setup (cherry picked from commit 5803819ba65e7211a4f49318d0d3bbf2246e21c1) --- dspace-api/src/main/java/org/dspace/core/Utils.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index b90e0d0fc306..2327a3f3fbc6 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -113,9 +113,6 @@ public final class Utils { "dspace.name", "dspace.shortname", "dspace.ui.url", "mail.helpdesk", "mail.message.helpdesk.telephone", "mail.admin", "mail.admin.name"}; - private static final ConfigurationService configurationService = - DSpaceServicesFactory.getInstance().getConfigurationService(); - /** * Private constructor */ @@ -530,6 +527,8 @@ public static String interpolateConfigsInString(String string) { public static Map getAllowedTemplateConfig() { // Pass a restricted (via configuration) list of resolved Configuration keys and values, for // template lookup + ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); List allowedConfigurationKeys = List.of(configurationService.getArrayProperty( "message.templates.allowed-config", DEFAULT_ALLOWED_TEMPLATE_CONFIGS)); return allowedConfigurationKeys.stream() From 2e5885f025e7fab00a7247865998ae825c40957b Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 10:28:41 +0200 Subject: [PATCH 680/701] Email configuationService access lazily (cherry picked from commit c70dc74d1d581b7d79d52aec1a3be863c2af68c8) --- .../src/main/java/org/dspace/core/Email.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Email.java b/dspace-api/src/main/java/org/dspace/core/Email.java index da2a7403fbaa..b1ffde70f1d2 100644 --- a/dspace-api/src/main/java/org/dspace/core/Email.java +++ b/dspace-api/src/main/java/org/dspace/core/Email.java @@ -170,9 +170,6 @@ public class Email { private static final Logger LOG = LogManager.getLogger(); - private static final ConfigurationService configurationService = - DSpaceServicesFactory.getInstance().getConfigurationService(); - /** Velocity template settings. */ private static final String RESOURCE_REPOSITORY_NAME = "Email"; @@ -196,6 +193,13 @@ public Email() { charset = null; } + /** + * Get configuration service + */ + private static ConfigurationService getConfigurationService() { + return DSpaceServicesFactory.getInstance().getConfigurationService(); + } + /** * Add a recipient. * @@ -368,11 +372,11 @@ void build() } // Get the mail configuration properties - String from = configurationService.getProperty("mail.from.address"); + String from = getConfigurationService().getProperty("mail.from.address"); // If no character set specified, attempt to retrieve a default if (charset == null) { - charset = configurationService.getProperty("mail.charset"); + charset = getConfigurationService().getProperty("mail.charset"); } // Get session @@ -387,7 +391,7 @@ void build() new InternetAddress(recipient)); } // Get headers defined by the template. - String[] templateHeaders = configurationService.getArrayProperty("mail.message.headers"); + String[] templateHeaders = getConfigurationService().getArrayProperty("mail.message.headers"); // Format the mail message body VelocityContext vctx = new VelocityContext(); From 57f5335ff6456532a5a8561080c8b7f867d9bdec Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 15:32:01 +0200 Subject: [PATCH 681/701] tidy access of configuration service --- .../src/main/java/org/dspace/core/Email.java | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Email.java b/dspace-api/src/main/java/org/dspace/core/Email.java index b1ffde70f1d2..f25c5b2a795a 100644 --- a/dspace-api/src/main/java/org/dspace/core/Email.java +++ b/dspace-api/src/main/java/org/dspace/core/Email.java @@ -339,10 +339,7 @@ public void reset() { */ public void send() throws MessagingException, IOException { build(); - - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - boolean disabled = config.getBooleanProperty("mail.server.disabled", false); + boolean disabled = getConfigurationService().getBooleanProperty("mail.server.disabled", false); if (disabled) { LOG.info(format(message, body)); } else { @@ -575,12 +572,10 @@ public static Email getEmail(String emailFile) * message is sent. */ public static void main(String[] args) { - ConfigurationService config - = DSpaceServicesFactory.getInstance().getConfigurationService(); - String to = config.getProperty("mail.admin"); + String to = getConfigurationService().getProperty("mail.admin"); String subject = "DSpace test email"; - String server = config.getProperty("mail.server"); - String url = config.getProperty("dspace.ui.url"); + String server = getConfigurationService().getProperty("mail.server"); + String url = getConfigurationService().getProperty("dspace.ui.url"); Email message; try { if (args.length <= 0) { @@ -598,7 +593,7 @@ public static void main(String[] args) { System.out.println(" - To: " + to); System.out.println(" - Subject: " + subject); System.out.println(" - Server: " + server); - boolean disabled = config.getBooleanProperty("mail.server.disabled", false); + boolean disabled = getConfigurationService().getBooleanProperty("mail.server.disabled", false); if (disabled) { System.err.println("\nError sending email:"); System.err.println(" - Error: cannot test email because mail.server.disabled is set to true"); From 5a0a29b440b467f0df313fda37aa58838f678e1e Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 15:40:09 +0200 Subject: [PATCH 682/701] Remove strict mode Velocity engine configuration (allow nulls) --- dspace-api/src/main/java/org/dspace/core/Utils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index 2327a3f3fbc6..3a65aafa48fe 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -569,7 +569,6 @@ public static Properties getSecureVelocityProperties(String resourceRepositoryNa "java.lang.Class,java.lang.Runtime,java.lang.System"); secureVelocityProperties.setProperty( "introspector.restrict.packages", "java.lang.reflect,java.io,java.nio"); - secureVelocityProperties.setProperty("runtime.strict_mode.enable", "true"); return secureVelocityProperties; } From b5c1979b0a63eb2aa73d7bd87f3f9385a2bf86c5 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 15:39:41 +0200 Subject: [PATCH 683/701] Remove strict mode Velocity engine configuration (allow nulls) --- dspace-api/src/main/java/org/dspace/core/Utils.java | 5 +++++ dspace/config/dspace.cfg | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/core/Utils.java b/dspace-api/src/main/java/org/dspace/core/Utils.java index 3a65aafa48fe..63369528a6e3 100644 --- a/dspace-api/src/main/java/org/dspace/core/Utils.java +++ b/dspace-api/src/main/java/org/dspace/core/Utils.java @@ -569,6 +569,11 @@ public static Properties getSecureVelocityProperties(String resourceRepositoryNa "java.lang.Class,java.lang.Runtime,java.lang.System"); secureVelocityProperties.setProperty( "introspector.restrict.packages", "java.lang.reflect,java.io,java.nio"); + // Set strict mode if configured (default: false, as we've always treated null values as blanks) + if (DSpaceServicesFactory.getInstance().getConfigurationService() + .getBooleanProperty("message.templates.strict_mode", false)) { + secureVelocityProperties.setProperty("runtime.strict_mode.enable", "true"); + } return secureVelocityProperties; } diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index cdd11e95c504..5917849ad634 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -240,6 +240,11 @@ message.templates.allowed-config = mail.message.helpdesk.telephone message.templates.allowed-config = mail.admin message.templates.allowed-config = mail.admin.name +# Whether to run Velocity in strict mode (null parameter values in templates for LDN or Email will result +# in an Exception instead of a blank string) +# Default: false (this can introduce unwanted side-effects if e.g. a submitter eperson is deleted for a workflow task) +#message.templates.strict_mode = false + ##### Asset Storage (bitstreams / files) ###### # Moved to config/spring/api/bitstore.xml From c00d6ae78d582746171a3e7514c8cc9a337f3e9d Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 6 May 2026 17:55:49 +0200 Subject: [PATCH 684/701] Curation I/O path safety Includes some central validation that other classes can make use of. However, it may be overly restrictive. And we may need to allow multiple separate absolute base paths for each config? --- .../main/java/org/dspace/curate/Curation.java | 25 +++- .../storage/secure/SecureFileAccess.java | 121 ++++++++++++++++++ dspace/config/modules/curate.cfg | 9 ++ 3 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index b894dcd85f03..16100eb38790 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -10,12 +10,12 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; -import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintStream; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.HashMap; import java.util.Iterator; @@ -38,6 +38,7 @@ import org.dspace.handle.service.HandleService; import org.dspace.scripts.DSpaceRunnable; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.storage.secure.SecureFileAccess; import org.dspace.utils.DSpace; /** @@ -113,7 +114,12 @@ private void handleCurationTask(Curator curator) throws IOException, SQLExceptio // load taskFile BufferedReader reader = null; try { - reader = new BufferedReader(new FileReader(this.taskFile)); + String dspaceDir = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("dspace.dir"); + String allowedTaskFileBasePath = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("curate.taskfile.base", dspaceDir); + reader = SecureFileAccess.getBufferedReader(this.taskFile, allowedTaskFileBasePath, + "curation-taskfile", StandardCharsets.UTF_8); while ((taskName = reader.readLine()) != null) { if (verbose) { super.handler.logInfo("Adding task: " + taskName); @@ -189,12 +195,23 @@ private void endScript(long timeRun) throws SQLException { private Curator initCurator() throws FileNotFoundException { Curator curator = new Curator(handler); OutputStream reporterStream; + String dspaceDir = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("dspace.dir"); + String allowedReporterBasePath = DSpaceServicesFactory.getInstance() + .getConfigurationService().getProperty("curate.reporter.base", + dspaceDir + File.separatorChar + "log"); if (null == this.reporter) { - reporterStream = NullOutputStream.NULL_OUTPUT_STREAM; + reporterStream = NullOutputStream.INSTANCE; } else if ("-".equals(this.reporter)) { reporterStream = System.out; } else { - reporterStream = new PrintStream(this.reporter); + try { + reporterStream = new PrintStream( + SecureFileAccess.getOutputStream( + this.reporter, allowedReporterBasePath, "curation-reporter")); + } catch (IOException e) { + throw new FileNotFoundException(e.getLocalizedMessage()); + } } Writer reportWriter = new OutputStreamWriter(reporterStream); curator.setReporter(reportWriter); diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java new file mode 100644 index 000000000000..dbe21520f2fd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -0,0 +1,121 @@ +/** + * 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.storage.secure; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; + +/** + * Decent I/O path validation - not perfect when symlinks are used and we are writing + * as 'toRealPath' check on the resolved path fails for new files + * + * @author Kim Shepherd + */ +public final class SecureFileAccess { + + private SecureFileAccess() {} + + /** + * Validate a given path against an allowed base path. Does not attempt to calculate "real path" + * before validation, as this breaks for new files which don't yet exist. This can make the resulting + * validation still vulnerable to symlink traversal in some cases + * @param file the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static Path validatePathForWrite(String file, String allowedBasePath, String purpose) + throws IOException { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).normalize(); + if (!resolvedPath.startsWith(basePath)) { + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + } + return resolvedPath; + } + + /** + * Validate a given path against an allowed base path. + * More secure than the 'write' variant because we can explicitly resolve links as well. + * + * @param file the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static Path validatePathForRead(String file, String allowedBasePath, String purpose) + throws IOException { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); + if (!resolvedPath.startsWith(basePath)) { + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + } + return resolvedPath; + } + + /** + * Get a buffered reader after validating file path. + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static BufferedReader getBufferedReader(String unvalidatedFile, String allowedBasePath, + String purpose, Charset charset) throws IOException { + if (charset == null) { + charset = StandardCharsets.UTF_8; + } + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePath, purpose); + return Files.newBufferedReader(validatedFile, charset); + } + + /** + * Get an input stream after validating file path. + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static InputStream getInputStream(String unvalidatedFile, String allowedBasePath, String purpose) + throws IOException { + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePath, purpose); + return Files.newInputStream(validatedFile); + + } + + /** + * Get an input stream after validating file path. Adds NOFOLLOW_LINKS link option to + * help ensure some extra safety since new files can't use toRealPath() for link calculation + * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration + * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param purpose the name of the calling component / use case for logging and inspection + * @throws IOException on validation failure + */ + public static OutputStream getOutputStream(String unvalidatedFile, String allowedBasePath, String purpose) + throws IOException { + Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePath, purpose); + return Files.newOutputStream(validatedFile, LinkOption.NOFOLLOW_LINKS); + } +} diff --git a/dspace/config/modules/curate.cfg b/dspace/config/modules/curate.cfg index 6e75738de543..68c6ea88fed6 100644 --- a/dspace/config/modules/curate.cfg +++ b/dspace/config/modules/curate.cfg @@ -29,3 +29,12 @@ curate.taskqueue.dir = ${dspace.dir}/ctqueues # Maximum amount of redirects set to 0 for none and -1 for unlimited curate.checklinks.max-redirect = 0 + +# allowed base path of curation task files +# it is recommended to restrict this path as much as possible +# so that the DSpace Processes framework may only load files as "tasks" +# from a trusted location +curate.taskfile.base = ${dspace.dir} + +# allowed base path of reporter output. +curate.reporter.base = ${dspace.dir}/log From 66205def24e5edde3557d40d3b21ca2cb9c0b3ad Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sun, 3 May 2026 14:56:24 +0200 Subject: [PATCH 685/701] Restrict LDN message templates to configured path # Conflicts: # dspace-api/src/main/java/org/dspace/core/LDN.java --- .../src/main/java/org/dspace/core/LDN.java | 20 +++++++++++++- .../ldn/action/SendLDNMessageActionIT.java | 27 +++++++++++++++++++ dspace/config/modules/ldn.cfg | 5 ++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java index c0c5690817cf..0aa4ec69cc26 100644 --- a/dspace-api/src/main/java/org/dspace/core/LDN.java +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -10,14 +10,18 @@ import static org.apache.commons.lang3.StringUtils.EMPTY; import java.io.BufferedReader; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Properties; import jakarta.mail.MessagingException; import org.apache.commons.lang3.StringUtils; @@ -56,6 +60,14 @@ public class LDN { /** Velocity template for the message*/ private Template template; + /** Allowed base directory for LDN messages / templates **/ + private static final ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + private static final String dspaceDir = configurationService.getProperty("dspace.dir", "/dspace"); + private static final Path allowedTemplateBasePath = Paths.get( + configurationService.getProperty( "ldn.template.path", dspaceDir + File.separatorChar + + "config" + File.separatorChar + "ldn")); + /** * Create a new ldn message. */ @@ -144,8 +156,14 @@ public String generateLDNMessage() { public static LDN getLDNMessage(String ldnMessageFile) throws IOException { StringBuilder contentBuffer = new StringBuilder(); + Path ldnMessagePath = new File(ldnMessageFile).toPath().normalize(); + Path realLdnMessagePath = allowedTemplateBasePath.resolve(ldnMessagePath).normalize(); + if (!realLdnMessagePath.startsWith(allowedTemplateBasePath)) { + throw new IOException("Illegal LDN message path: '" + ldnMessagePath + "'"); + } + try ( - InputStream is = new FileInputStream(ldnMessageFile); + InputStream is = new FileInputStream(realLdnMessagePath.toFile()); InputStreamReader ir = new InputStreamReader(is, "UTF-8"); BufferedReader reader = new BufferedReader(ir); ) { diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java index 73f97b2a6a7c..0be888d92b05 100644 --- a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java +++ b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java @@ -10,10 +10,14 @@ import static org.dspace.app.ldn.action.LDNActionStatus.ABORT; import static org.dspace.app.ldn.action.LDNActionStatus.CONTINUE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.List; @@ -40,6 +44,7 @@ import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; +import org.dspace.core.LDN; import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -232,6 +237,28 @@ public void testLDNMessageConsumerRequestReviewWithInvalidLdnUrl() throws Except response.close(); } + @Test + public void testLDNLegalPath() throws Exception { + try { + // Path traversal will be calculated properly + // but this still ends up at a legal base + LDN message = LDN.getLDNMessage("../../config/ldn/request-review"); + assertNotNull(message); + } catch (IOException e) { + fail("IOException should NOT have been thrown for legal template path: " + e.getMessage()); + } + } + + @Test + public void testLDNIllegalPath() throws Exception { + try { + LDN.getLDNMessage("../modules/dspace.cfg"); + fail("IOException should have been thrown for illegal template path"); + } catch (IOException e) { + assertTrue(e.getMessage().contains("Illegal LDN message path:")); + } + } + @Override @After public void destroy() throws Exception { diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg index 96d4e39ffc11..377de5ee8fa1 100644 --- a/dspace/config/modules/ldn.cfg +++ b/dspace/config/modules/ldn.cfg @@ -40,6 +40,11 @@ ldn.notify.inbox.block-untrusted-ip = true # this is the medatada used to retrieve the relation with external items when sending relationship requests #ldn.notify.relation.metadata = dc.relation +# Allowed base path of LDN templates. Apache Velocity will only +# load templates that begin with this base path. +# Default is ${dspace.dir}/config/ldn +#ldn.template.path = ${dspace.dir}/config/ldn + # EMAIL CONFIGURATION # Supported values for actionSendFilter are: From c8fa44dc451aea3d3ec26076f3fe1f742a1c91c7 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 16 May 2026 16:41:05 +0200 Subject: [PATCH 686/701] Use new SecureFileAccess validator for LDN --- .../src/main/java/org/dspace/core/LDN.java | 22 +++--- .../main/java/org/dspace/curate/Curation.java | 16 ++-- .../storage/secure/SecureFileAccess.java | 74 +++++++++++-------- dspace/config/modules/curate.cfg | 5 +- dspace/config/modules/ldn.cfg | 1 + 5 files changed, 65 insertions(+), 53 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java index 0aa4ec69cc26..d801633f599a 100644 --- a/dspace-api/src/main/java/org/dspace/core/LDN.java +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -11,14 +11,12 @@ import java.io.BufferedReader; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Properties; @@ -34,6 +32,9 @@ import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; import org.apache.velocity.runtime.resource.util.StringResourceRepository; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.storage.secure.SecureFileAccess; /** * Class representing an LDN message json @@ -64,9 +65,8 @@ public class LDN { private static final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); private static final String dspaceDir = configurationService.getProperty("dspace.dir", "/dspace"); - private static final Path allowedTemplateBasePath = Paths.get( - configurationService.getProperty( "ldn.template.path", dspaceDir + File.separatorChar - + "config" + File.separatorChar + "ldn")); + private static final String[] DEFAULT_TEMPLATE_PATHS = new String[]{ + dspaceDir + File.separatorChar + "config" + File.separatorChar + "ldn"}; /** * Create a new ldn message. @@ -156,14 +156,10 @@ public String generateLDNMessage() { public static LDN getLDNMessage(String ldnMessageFile) throws IOException { StringBuilder contentBuffer = new StringBuilder(); - Path ldnMessagePath = new File(ldnMessageFile).toPath().normalize(); - Path realLdnMessagePath = allowedTemplateBasePath.resolve(ldnMessagePath).normalize(); - if (!realLdnMessagePath.startsWith(allowedTemplateBasePath)) { - throw new IOException("Illegal LDN message path: '" + ldnMessagePath + "'"); - } - + List allowedBasePaths = Arrays.stream(configurationService + .getArrayProperty("ldn.template.path", DEFAULT_TEMPLATE_PATHS)).toList(); try ( - InputStream is = new FileInputStream(realLdnMessagePath.toFile()); + InputStream is = SecureFileAccess.getInputStream(ldnMessageFile, allowedBasePaths, "ldn"); InputStreamReader ir = new InputStreamReader(is, "UTF-8"); BufferedReader reader = new BufferedReader(ir); ) { diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index 16100eb38790..3b53906175de 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -19,6 +19,7 @@ import java.sql.SQLException; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -116,8 +117,10 @@ private void handleCurationTask(Curator curator) throws IOException, SQLExceptio try { String dspaceDir = DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("dspace.dir"); - String allowedTaskFileBasePath = DSpaceServicesFactory.getInstance() - .getConfigurationService().getProperty("curate.taskfile.base", dspaceDir); + List allowedTaskFileBasePath = Arrays.stream( + DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("curate.taskfile.base", new String[]{dspaceDir}) + ).toList(); reader = SecureFileAccess.getBufferedReader(this.taskFile, allowedTaskFileBasePath, "curation-taskfile", StandardCharsets.UTF_8); while ((taskName = reader.readLine()) != null) { @@ -197,9 +200,10 @@ private Curator initCurator() throws FileNotFoundException { OutputStream reporterStream; String dspaceDir = DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("dspace.dir"); - String allowedReporterBasePath = DSpaceServicesFactory.getInstance() - .getConfigurationService().getProperty("curate.reporter.base", - dspaceDir + File.separatorChar + "log"); + List allowedReporterBasePaths = Arrays.stream( + DSpaceServicesFactory.getInstance() + .getConfigurationService().getArrayProperty("curate.reporter.base", + new String[]{dspaceDir + File.separatorChar + "log"})).toList(); if (null == this.reporter) { reporterStream = NullOutputStream.INSTANCE; } else if ("-".equals(this.reporter)) { @@ -208,7 +212,7 @@ private Curator initCurator() throws FileNotFoundException { try { reporterStream = new PrintStream( SecureFileAccess.getOutputStream( - this.reporter, allowedReporterBasePath, "curation-reporter")); + this.reporter, allowedReporterBasePaths, "curation-reporter")); } catch (IOException e) { throw new FileNotFoundException(e.getLocalizedMessage()); } diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java index dbe21520f2fd..2a902ddf5ea6 100644 --- a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -16,6 +16,7 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.List; /** * Decent I/O path validation - not perfect when symlinks are used and we are writing @@ -32,23 +33,28 @@ private SecureFileAccess() {} * before validation, as this breaks for new files which don't yet exist. This can make the resulting * validation still vulnerable to symlink traversal in some cases * @param file the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths list of allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static Path validatePathForWrite(String file, String allowedBasePath, String purpose) + public static Path validatePathForWrite(String file, List allowedBasePaths, String purpose) throws IOException { - Path basePath = Path.of(allowedBasePath) - .toRealPath() - .normalize(); - if (basePath == null) { - throw new IOException("Null base path can not be provided for validation"); - } - Path resolvedPath = basePath.resolve(file).normalize(); - if (!resolvedPath.startsWith(basePath)) { - throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + for (String allowedBasePath : allowedBasePaths) { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).normalize(); + if (resolvedPath.startsWith(basePath)) { + return resolvedPath; + } } - return resolvedPath; + + // If no valid path was resolved and returned by now + // we raise an exception and treat this as illegal access + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); } /** @@ -60,47 +66,51 @@ public static Path validatePathForWrite(String file, String allowedBasePath, Str * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static Path validatePathForRead(String file, String allowedBasePath, String purpose) + public static Path validatePathForRead(String file, List allowedBasePaths, String purpose) throws IOException { - Path basePath = Path.of(allowedBasePath) - .toRealPath() - .normalize(); - if (basePath == null) { - throw new IOException("Null base path can not be provided for validation"); - } - Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); - if (!resolvedPath.startsWith(basePath)) { - throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); + for (String allowedBasePath : allowedBasePaths) { + Path basePath = Path.of(allowedBasePath) + .toRealPath() + .normalize(); + if (basePath == null) { + throw new IOException("Null base path can not be provided for validation"); + } + Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); + if (resolvedPath.startsWith(basePath)) { + return resolvedPath; + } } - return resolvedPath; + // If no valid path was resolved and returned by now + // we raise an exception and treat this as illegal access + throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); } /** * Get a buffered reader after validating file path. * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static BufferedReader getBufferedReader(String unvalidatedFile, String allowedBasePath, + public static BufferedReader getBufferedReader(String unvalidatedFile, List allowedBasePaths, String purpose, Charset charset) throws IOException { if (charset == null) { charset = StandardCharsets.UTF_8; } - Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePath, purpose); + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePaths, purpose); return Files.newBufferedReader(validatedFile, charset); } /** * Get an input stream after validating file path. * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static InputStream getInputStream(String unvalidatedFile, String allowedBasePath, String purpose) + public static InputStream getInputStream(String unvalidatedFile, List allowedBasePaths, String purpose) throws IOException { - Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePath, purpose); + Path validatedFile = validatePathForRead(unvalidatedFile, allowedBasePaths, purpose); return Files.newInputStream(validatedFile); } @@ -109,13 +119,13 @@ public static InputStream getInputStream(String unvalidatedFile, String allowedB * Get an input stream after validating file path. Adds NOFOLLOW_LINKS link option to * help ensure some extra safety since new files can't use toRealPath() for link calculation * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ - public static OutputStream getOutputStream(String unvalidatedFile, String allowedBasePath, String purpose) + public static OutputStream getOutputStream(String unvalidatedFile, List allowedBasePaths, String purpose) throws IOException { - Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePath, purpose); + Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePaths, purpose); return Files.newOutputStream(validatedFile, LinkOption.NOFOLLOW_LINKS); } } diff --git a/dspace/config/modules/curate.cfg b/dspace/config/modules/curate.cfg index 68c6ea88fed6..26938893b25c 100644 --- a/dspace/config/modules/curate.cfg +++ b/dspace/config/modules/curate.cfg @@ -30,10 +30,11 @@ curate.taskqueue.dir = ${dspace.dir}/ctqueues # Maximum amount of redirects set to 0 for none and -1 for unlimited curate.checklinks.max-redirect = 0 -# allowed base path of curation task files +# allowed base path(s) of curation task files # it is recommended to restrict this path as much as possible # so that the DSpace Processes framework may only load files as "tasks" -# from a trusted location +# from a trusted location. For multiple paths, repeat this configuration +# property for each trusted path curate.taskfile.base = ${dspace.dir} # allowed base path of reporter output. diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg index 377de5ee8fa1..d33cdf8356ad 100644 --- a/dspace/config/modules/ldn.cfg +++ b/dspace/config/modules/ldn.cfg @@ -42,6 +42,7 @@ ldn.notify.inbox.block-untrusted-ip = true # Allowed base path of LDN templates. Apache Velocity will only # load templates that begin with this base path. +# You can repeat this parameter to allow multiple base paths. # Default is ${dspace.dir}/config/ldn #ldn.template.path = ${dspace.dir}/config/ldn From c4beefa3815ef9933b480ac7acb0fc0ac14f80e2 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 16 May 2026 17:08:59 +0200 Subject: [PATCH 687/701] Update LDN file path test --- .../java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java index 0be888d92b05..1a04d795684c 100644 --- a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java +++ b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java @@ -255,7 +255,7 @@ public void testLDNIllegalPath() throws Exception { LDN.getLDNMessage("../modules/dspace.cfg"); fail("IOException should have been thrown for illegal template path"); } catch (IOException e) { - assertTrue(e.getMessage().contains("Illegal LDN message path:")); + assertTrue(e.getMessage().contains("Illegal file path attempted for I/O (ldn):")); } } From 41d67dd24584ef268a54f6e87a38ac86bded4ced Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 26 May 2026 14:06:14 +0200 Subject: [PATCH 688/701] Move curation -r reporter param to CLI only --- .../java/org/dspace/curate/CurationCliScriptConfiguration.java | 3 +++ .../src/main/java/org/dspace/curate/CurationClientOptions.java | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java index eaa04f477829..07684f3643d0 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java @@ -20,6 +20,9 @@ public Options getOptions() { options = super.getOptions(); options.addOption("e", "eperson", true, "email address of curating eperson"); options.getOption("e").setRequired(true); + options.addOption("r", "reporter", true, + "relative or absolute path to the desired report file. Use '-' to report to console. If absent, no " + + "reporting"); return options; } } diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java index 8ec0f14697c0..a8a3d358ce67 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java @@ -59,9 +59,6 @@ protected static Options constructOptions() { "Id (handle) of object to perform task on, or 'all' to perform on whole repository"); options.addOption("p", "parameter", true, "a task parameter 'NAME=VALUE'"); options.addOption("q", "queue", true, "name of task queue to process"); - options.addOption("r", "reporter", true, - "relative or absolute path to the desired report file. Use '-' to report to console. If absent, no " + - "reporting"); options.addOption("s", "scope", true, "transaction scope to impose: use 'object', 'curation', or 'open'. If absent, 'open' applies"); options.addOption("v", "verbose", false, "report activity to stdout"); From 6e34d327f20148528e19941ad892f65caf092cbd Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 26 May 2026 14:30:00 +0200 Subject: [PATCH 689/701] Improve SecureFileAccess handling --- .../org/dspace/storage/secure/SecureFileAccess.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java index 2a902ddf5ea6..073604a9bdf5 100644 --- a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -14,7 +14,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.Path; import java.util.List; @@ -62,7 +61,7 @@ public static Path validatePathForWrite(String file, List allowedBasePat * More secure than the 'write' variant because we can explicitly resolve links as well. * * @param file the unvalidated file, usually derived from user input or configuration - * @param allowedBasePath the allowed base path for this use case as per system configuration + * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ @@ -72,9 +71,6 @@ public static Path validatePathForRead(String file, List allowedBasePath Path basePath = Path.of(allowedBasePath) .toRealPath() .normalize(); - if (basePath == null) { - throw new IOException("Null base path can not be provided for validation"); - } Path resolvedPath = basePath.resolve(file).toRealPath().normalize(); if (resolvedPath.startsWith(basePath)) { return resolvedPath; @@ -116,8 +112,8 @@ public static InputStream getInputStream(String unvalidatedFile, List al } /** - * Get an input stream after validating file path. Adds NOFOLLOW_LINKS link option to - * help ensure some extra safety since new files can't use toRealPath() for link calculation + * Get an output stream after validating file path. New files can't use toRealPath() for link calculation so + * there is a bit of a trade-off in allowing some symlink traversal to occur * @param unvalidatedFile the unvalidated file, usually derived from user input or configuration * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection @@ -126,6 +122,6 @@ public static InputStream getInputStream(String unvalidatedFile, List al public static OutputStream getOutputStream(String unvalidatedFile, List allowedBasePaths, String purpose) throws IOException { Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePaths, purpose); - return Files.newOutputStream(validatedFile, LinkOption.NOFOLLOW_LINKS); + return Files.newOutputStream(validatedFile); } } From ab55baa60586c3f94f1300f0b5309a6fc313df46 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Tue, 26 May 2026 18:48:10 +0200 Subject: [PATCH 690/701] Require abs path in SecureFileAccess, calc in callers --- .../src/main/java/org/dspace/core/LDN.java | 13 ++++-- .../main/java/org/dspace/curate/Curation.java | 24 +++++----- .../storage/secure/SecureFileAccess.java | 44 ++++++++++++++++++- .../ldn/action/SendLDNMessageActionIT.java | 7 ++- dspace/config/modules/ldn.cfg | 7 ++- 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java index d801633f599a..19be26c586c9 100644 --- a/dspace-api/src/main/java/org/dspace/core/LDN.java +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -19,7 +19,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Properties; import jakarta.mail.MessagingException; import org.apache.commons.lang3.StringUtils; @@ -156,10 +155,16 @@ public String generateLDNMessage() { public static LDN getLDNMessage(String ldnMessageFile) throws IOException { StringBuilder contentBuffer = new StringBuilder(); - List allowedBasePaths = Arrays.stream(configurationService - .getArrayProperty("ldn.template.path", DEFAULT_TEMPLATE_PATHS)).toList(); + List allowedBasePaths = List.of( + Arrays.stream(configurationService + .getArrayProperty("ldn.template.path", DEFAULT_TEMPLATE_PATHS)) + .findFirst() + .orElseThrow(() -> new IOException("No LDN template path configured")) + ); + String ldnFilePath = SecureFileAccess.calculateAbsolutePathUsingBaseDir(ldnMessageFile, + allowedBasePaths.get(0)); try ( - InputStream is = SecureFileAccess.getInputStream(ldnMessageFile, allowedBasePaths, "ldn"); + InputStream is = SecureFileAccess.getInputStream(ldnFilePath, allowedBasePaths, "ldn"); InputStreamReader ir = new InputStreamReader(is, "UTF-8"); BufferedReader reader = new BufferedReader(ir); ) { diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java index 3b53906175de..35f6fbee6916 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curation.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java @@ -17,6 +17,8 @@ import java.io.Writer; import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -114,14 +116,15 @@ private void handleCurationTask(Curator curator) throws IOException, SQLExceptio } else if (commandLine.hasOption('T')) { // load taskFile BufferedReader reader = null; + // in this case, Curation CLI expects to calculate the -T parameter from the user's current working dir + String taskFilePath = SecureFileAccess.calculateAbsolutePathUsingCwd(this.taskFile); try { String dspaceDir = DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("dspace.dir"); - List allowedTaskFileBasePath = Arrays.stream( - DSpaceServicesFactory.getInstance().getConfigurationService() - .getArrayProperty("curate.taskfile.base", new String[]{dspaceDir}) - ).toList(); - reader = SecureFileAccess.getBufferedReader(this.taskFile, allowedTaskFileBasePath, + List allowedTaskFileBasePath = new ArrayList<>( + Arrays.asList(DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("curate.taskfile.base", new String[]{dspaceDir}))); + reader = SecureFileAccess.getBufferedReader(taskFilePath, allowedTaskFileBasePath, "curation-taskfile", StandardCharsets.UTF_8); while ((taskName = reader.readLine()) != null) { if (verbose) { @@ -200,19 +203,20 @@ private Curator initCurator() throws FileNotFoundException { OutputStream reporterStream; String dspaceDir = DSpaceServicesFactory.getInstance() .getConfigurationService().getProperty("dspace.dir"); - List allowedReporterBasePaths = Arrays.stream( - DSpaceServicesFactory.getInstance() - .getConfigurationService().getArrayProperty("curate.reporter.base", - new String[]{dspaceDir + File.separatorChar + "log"})).toList(); + List allowedReporterBasePaths = new ArrayList<>(Arrays.asList(DSpaceServicesFactory.getInstance() + .getConfigurationService().getArrayProperty("curate.reporter.base", + new String[]{dspaceDir + File.separatorChar + "log"}))); if (null == this.reporter) { reporterStream = NullOutputStream.INSTANCE; } else if ("-".equals(this.reporter)) { reporterStream = System.out; } else { + // Reporter param comes from CLI execution. Calculate abs path from user's current working dir + String reporterFilePath = SecureFileAccess.calculateAbsolutePathUsingCwd(this.reporter); try { reporterStream = new PrintStream( SecureFileAccess.getOutputStream( - this.reporter, allowedReporterBasePaths, "curation-reporter")); + reporterFilePath, allowedReporterBasePaths, "curation-reporter")); } catch (IOException e) { throw new FileNotFoundException(e.getLocalizedMessage()); } diff --git a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java index 073604a9bdf5..3d70831b0b9f 100644 --- a/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java +++ b/dspace-api/src/main/java/org/dspace/storage/secure/SecureFileAccess.java @@ -32,12 +32,18 @@ private SecureFileAccess() {} * before validation, as this breaks for new files which don't yet exist. This can make the resulting * validation still vulnerable to symlink traversal in some cases * @param file the unvalidated file, usually derived from user input or configuration + * This MUST be an absolute path, and the caller is expected to calculate it based on best + * context (e.g. configured base path, CWD, dspace.dir, and so on) * @param allowedBasePaths list of allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ public static Path validatePathForWrite(String file, List allowedBasePaths, String purpose) throws IOException { + Path filePath = Path.of(file); + if (!filePath.isAbsolute()) { + throw new IOException("Absolute path required for I/O (%s): %s".formatted(purpose, file)); + } for (String allowedBasePath : allowedBasePaths) { Path basePath = Path.of(allowedBasePath) .toRealPath() @@ -50,7 +56,7 @@ public static Path validatePathForWrite(String file, List allowedBasePat return resolvedPath; } } - + // If no valid path was resolved and returned by now // we raise an exception and treat this as illegal access throw new IOException("Illegal file path attempted for I/O (%s): %s".formatted(purpose, file)); @@ -61,12 +67,18 @@ public static Path validatePathForWrite(String file, List allowedBasePat * More secure than the 'write' variant because we can explicitly resolve links as well. * * @param file the unvalidated file, usually derived from user input or configuration + * This MUST be an absolute path, and the caller is expected to calculate it based on best + * context (e.g. configured base path, CWD, dspace.dir, and so on) * @param allowedBasePaths the allowed base paths for this use case as per system configuration * @param purpose the name of the calling component / use case for logging and inspection * @throws IOException on validation failure */ public static Path validatePathForRead(String file, List allowedBasePaths, String purpose) throws IOException { + Path filePath = Path.of(file); + if (!filePath.isAbsolute()) { + throw new IOException("Absolute path required for I/O (%s): %s".formatted(purpose, file)); + } for (String allowedBasePath : allowedBasePaths) { Path basePath = Path.of(allowedBasePath) .toRealPath() @@ -124,4 +136,34 @@ public static OutputStream getOutputStream(String unvalidatedFile, List Path validatedFile = validatePathForWrite(unvalidatedFile, allowedBasePaths, purpose); return Files.newOutputStream(validatedFile); } + + /** + * Calculate an absolute path (if not already absolute) using current working dir as a root + * for relative file paths + * @param file the relative or absolute file given as input + * @return absolute path calculated from file and cwd + */ + public static String calculateAbsolutePathUsingCwd(String file) { + String filePath = file; + Path path = Path.of(filePath); + if (!path.isAbsolute()) { + filePath = Path.of("").toAbsolutePath().resolve(path).normalize().toString(); + } + return filePath; + } + + /** + * Calculate an absolute path (if not already absolute) using a given base dir as a root + * for relative file paths + * @param file the relative or absolute file given as input + * @return absolute path calculated from file and base dir + */ + public static String calculateAbsolutePathUsingBaseDir(String file, String baseDir) { + String filePath = file; + Path path = Path.of(filePath); + if (!path.isAbsolute()) { + filePath = Path.of(baseDir).toAbsolutePath().resolve(path).normalize().toString(); + } + return filePath; + } } diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java index 1a04d795684c..bec0de59619c 100644 --- a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java +++ b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.sql.SQLException; import java.util.List; @@ -252,7 +253,11 @@ public void testLDNLegalPath() throws Exception { @Test public void testLDNIllegalPath() throws Exception { try { - LDN.getLDNMessage("../modules/dspace.cfg"); + String badAbsolutePath = Path.of(configurationService.getProperty("dspace.dir")) + .resolve("config/dspace.cfg") + .toAbsolutePath() + .toString(); + LDN.getLDNMessage(badAbsolutePath); fail("IOException should have been thrown for illegal template path"); } catch (IOException e) { assertTrue(e.getMessage().contains("Illegal file path attempted for I/O (ldn):")); diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg index d33cdf8356ad..8219070cccf3 100644 --- a/dspace/config/modules/ldn.cfg +++ b/dspace/config/modules/ldn.cfg @@ -40,13 +40,12 @@ ldn.notify.inbox.block-untrusted-ip = true # this is the medatada used to retrieve the relation with external items when sending relationship requests #ldn.notify.relation.metadata = dc.relation -# Allowed base path of LDN templates. Apache Velocity will only -# load templates that begin with this base path. -# You can repeat this parameter to allow multiple base paths. +# Base path of LDN templates. Apache Velocity will only +# load templates that begin with this base path, and relative paths will use this as a base when +# calculating the absolute path. This is not repeatable, multiple paths will be ignored beyond the first. # Default is ${dspace.dir}/config/ldn #ldn.template.path = ${dspace.dir}/config/ldn - # EMAIL CONFIGURATION # Supported values for actionSendFilter are: # single email, "GROUP:" or "SUBMITTER" From e80744077c798f9ec9d0578b3aca0d95c9bc5932 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 06:40:16 +0200 Subject: [PATCH 691/701] Comment out default curation dir properties --- dspace/config/modules/curate.cfg | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dspace/config/modules/curate.cfg b/dspace/config/modules/curate.cfg index 26938893b25c..dad40d615454 100644 --- a/dspace/config/modules/curate.cfg +++ b/dspace/config/modules/curate.cfg @@ -35,7 +35,9 @@ curate.checklinks.max-redirect = 0 # so that the DSpace Processes framework may only load files as "tasks" # from a trusted location. For multiple paths, repeat this configuration # property for each trusted path -curate.taskfile.base = ${dspace.dir} +# Default: ${dspace.dir} +#curate.taskfile.base = ${dspace.dir} # allowed base path of reporter output. -curate.reporter.base = ${dspace.dir}/log +# Default: ${dspace.dir}/log +#curate.reporter.base = ${dspace.dir}/log From 8073591ca16d37cf92a819d2865b45ba4a1ba18e Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 14:38:00 +0200 Subject: [PATCH 692/701] Ignore CurationScriptIT -T taskFile tests, to rewrite w/ CLI (cherry picked from commit 6437472b8277b9aa815dd71e14b499ba7515f87d) --- .../src/test/java/org/dspace/curate/CurationScriptIT.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java b/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java index 8745613d7af6..347ed0935f05 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/curate/CurationScriptIT.java @@ -49,6 +49,7 @@ import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.service.ScriptService; +import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -214,6 +215,7 @@ public void curateScript_InvalidScope() throws Exception { .andExpect(status().isBadRequest()); } + @Ignore @Test public void curateScript_InvalidTaskFile() throws Exception { String token = getAuthToken(admin.getEmail(), password); @@ -286,6 +288,7 @@ public void curateScript_validRequest_Task() throws Exception { } } + @Ignore @Test public void curateScript_validRequest_TaskFile() throws Exception { context.turnOffAuthorisationSystem(); From 8c9511b7009195bb3824e7e41fe088b5294fe323 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 27 May 2026 06:43:22 +0200 Subject: [PATCH 693/701] Move taskfile -T option to CLI script config only (cherry picked from commit 00e4979a60fd69adbf4a7476926701ef59207ce7) --- .../org/dspace/curate/CurationCliScriptConfiguration.java | 1 + .../main/java/org/dspace/curate/CurationClientOptions.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java index 07684f3643d0..925bd4f2d232 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationCliScriptConfiguration.java @@ -23,6 +23,7 @@ public Options getOptions() { options.addOption("r", "reporter", true, "relative or absolute path to the desired report file. Use '-' to report to console. If absent, no " + "reporting"); + options.addOption("T", "taskfile", true, "file containing curation task names"); return options; } } diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java index a8a3d358ce67..03ad2f34b230 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationClientOptions.java @@ -31,7 +31,8 @@ public enum CurationClientOptions { /** * This method resolves the CommandLine parameters to figure out which action the curation script should perform * - * @param commandLine The relevant CommandLine for the curation script + * @param commandLine The relevant CommandLine for the curation script. Note that -T is passed only + * from CurationCliScriptConfig and is not accessible from UI processes * @return The curation option to be ran, parsed from the CommandLine */ protected static CurationClientOptions getClientOption(CommandLine commandLine) { @@ -54,7 +55,6 @@ protected static Options constructOptions() { Options options = new Options(); options.addOption("t", "task", true, "curation task name; options: " + getTaskOptions()); - options.addOption("T", "taskfile", true, "file containing curation task names"); options.addOption("i", "id", true, "Id (handle) of object to perform task on, or 'all' to perform on whole repository"); options.addOption("p", "parameter", true, "a task parameter 'NAME=VALUE'"); From 43fbf7d16d03256592611bf6a546ad85713e1e17 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Wed, 27 May 2026 13:48:55 -0500 Subject: [PATCH 694/701] Update LICENSES_THIRD_PARTY for next release --- LICENSES_THIRD_PARTY | 352 ++++++++++++++++++++++--------------------- 1 file changed, 178 insertions(+), 174 deletions(-) diff --git a/LICENSES_THIRD_PARTY b/LICENSES_THIRD_PARTY index 74f966caf188..c65827035560 100644 --- a/LICENSES_THIRD_PARTY +++ b/LICENSES_THIRD_PARTY @@ -21,34 +21,34 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines Apache Software License, Version 2.0: * Ant-Contrib Tasks (ant-contrib:ant-contrib:1.0b3 - http://ant-contrib.sourceforge.net) - * S3Mock - Testsupport - Testcontainers (com.adobe.testing:s3mock-testcontainers:4.10.0 - https://www.github.com/adobe/S3Mock/s3mock-testsupport-reactor/s3mock-testcontainers) + * S3Mock - Testsupport - Testcontainers (com.adobe.testing:s3mock-testcontainers:4.12.4 - https://www.github.com/adobe/S3Mock/s3mock-testsupport-reactor/s3mock-testcontainers) * Titanium JSON-LD 1.1 (JRE11) (com.apicatalog:titanium-json-ld:1.3.2 - https://github.com/filip26/titanium-json-ld) * HPPC Collections (com.carrotsearch:hppc:0.8.1 - http://labs.carrotsearch.com/hppc.html/hppc) * com.drewnoakes:metadata-extractor (com.drewnoakes:metadata-extractor:2.19.0 - https://drewnoakes.com/code/exif/) * parso (com.epam:parso:2.0.14 - https://github.com/epam/parso) * Internet Time Utility (com.ethlo.time:itu:1.7.0 - https://github.com/ethlo/itu) - * ClassMate (com.fasterxml:classmate:1.7.1 - https://github.com/FasterXML/java-classmate) - * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.20 - https://github.com/FasterXML/jackson) - * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.20.1 - https://github.com/FasterXML/jackson-core) - * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.20.1 - https://github.com/FasterXML/jackson) + * ClassMate (com.fasterxml:classmate:1.7.3 - https://github.com/FasterXML/java-classmate) + * Jackson-annotations (com.fasterxml.jackson.core:jackson-annotations:2.21 - https://github.com/FasterXML/jackson) + * Jackson-core (com.fasterxml.jackson.core:jackson-core:2.21.3 - https://github.com/FasterXML/jackson-core) + * jackson-databind (com.fasterxml.jackson.core:jackson-databind:2.21.3 - https://github.com/FasterXML/jackson) * Jackson dataformat: Smile (com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.15.2 - https://github.com/FasterXML/jackson-dataformats-binary) * Jackson-dataformat-TOML (com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.15.2 - https://github.com/FasterXML/jackson-dataformats-text) * Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.2 - https://github.com/FasterXML/jackson-dataformats-text) - * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.19.4 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) - * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.20.1 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) + * Jackson datatype: jdk8 (com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.21.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8) + * Jackson datatype: JSR310 (com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.3 - https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310) * Jackson Jakarta-RS: base (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-base:2.16.2 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-base) * Jackson Jakarta-RS: JSON (com.fasterxml.jackson.jakarta.rs:jackson-jakarta-rs-json-provider:2.16.2 - https://github.com/FasterXML/jackson-jakarta-rs-providers/jackson-jakarta-rs-json-provider) * Jackson module: Jakarta XML Bind Annotations (jakarta.xml.bind) (com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations:2.16.2 - https://github.com/FasterXML/jackson-modules-base) - * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.19.4 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) + * Jackson-module-parameter-names (com.fasterxml.jackson.module:jackson-module-parameter-names:2.21.2 - https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names) * Java UUID Generator (com.fasterxml.uuid:java-uuid-generator:4.1.0 - https://github.com/cowtowncoder/java-uuid-generator) * Woodstox (com.fasterxml.woodstox:woodstox-core:6.5.1 - https://github.com/FasterXML/woodstox) * zjsonpatch (com.flipkart.zjsonpatch:zjsonpatch:0.4.16 - https://github.com/flipkart-incubator/zjsonpatch/) * Caffeine cache (com.github.ben-manes.caffeine:caffeine:2.9.3 - https://github.com/ben-manes/caffeine) * Caffeine cache (com.github.ben-manes.caffeine:caffeine:3.1.8 - https://github.com/ben-manes/caffeine) * JSON.simple (com.github.cliftonlabs:json-simple:3.0.2 - https://cliftonlabs.github.io/json-simple/) - * docker-java-api (com.github.docker-java:docker-java-api:3.4.2 - https://github.com/docker-java/docker-java) - * docker-java-transport (com.github.docker-java:docker-java-transport:3.4.2 - https://github.com/docker-java/docker-java) - * docker-java-transport-zerodep (com.github.docker-java:docker-java-transport-zerodep:3.4.2 - https://github.com/docker-java/docker-java) + * docker-java-api (com.github.docker-java:docker-java-api:3.7.1 - https://github.com/docker-java/docker-java) + * docker-java-transport (com.github.docker-java:docker-java-transport:3.7.1 - https://github.com/docker-java/docker-java) + * docker-java-transport-zerodep (com.github.docker-java:docker-java-transport-zerodep:3.7.1 - https://github.com/docker-java/docker-java) * btf (com.github.java-json-tools:btf:1.3 - https://github.com/java-json-tools/btf) * jackson-coreutils (com.github.java-json-tools:jackson-coreutils:2.0 - https://github.com/java-json-tools/jackson-coreutils) * jackson-coreutils-equivalence (com.github.java-json-tools:jackson-coreutils-equivalence:1.0 - https://github.com/java-json-tools/jackson-coreutils) @@ -59,13 +59,14 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * uri-template (com.github.java-json-tools:uri-template:0.10 - https://github.com/java-json-tools/uri-template) * JCIP Annotations under Apache License (com.github.stephenc.jcip:jcip-annotations:1.0-1 - http://stephenc.github.com/jcip-annotations) * FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/) - * Gson (com.google.code.gson:gson:2.13.2 - https://github.com/google/gson) + * Gson (com.google.code.gson:gson:2.14.0 - https://github.com/google/gson) * error-prone annotations (com.google.errorprone:error_prone_annotations:2.42.0 - https://errorprone.info/error_prone_annotations) * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess) - * Guava: Google Core Libraries for Java (com.google.guava:guava:32.1.3-jre - https://github.com/google/guava) + * Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.3 - https://github.com/google/guava/failureaccess) + * Guava: Google Core Libraries for Java (com.google.guava:guava:33.6.0-jre - https://github.com/google/guava) * Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture) * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/) - * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:2.8 - https://github.com/google/j2objc/) + * J2ObjC Annotations (com.google.j2objc:j2objc-annotations:3.1 - https://github.com/google/j2objc/) * libphonenumber (com.googlecode.libphonenumber:libphonenumber:8.11.1 - https://github.com/google/libphonenumber/) * Jackcess (com.healthmarketscience.jackcess:jackcess:4.0.10 - https://jackcess.sourceforge.io) * Jackcess Encrypt (com.healthmarketscience.jackcess:jackcess-encrypt:4.0.3 - http://jackcessencrypt.sf.net) @@ -91,12 +92,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * SparseBitSet (com.zaxxer:SparseBitSet:1.3 - https://github.com/brettwooldridge/SparseBitSet) * Apache Commons BeanUtils (commons-beanutils:commons-beanutils:1.11.0 - https://commons.apache.org/proper/commons-beanutils) * Apache Commons CLI (commons-cli:commons-cli:1.11.0 - https://commons.apache.org/proper/commons-cli/) - * Apache Commons Codec (commons-codec:commons-codec:1.20.0 - https://commons.apache.org/proper/commons-codec/) + * Apache Commons Codec (commons-codec:commons-codec:1.22.0 - https://commons.apache.org/proper/commons-codec/) * Apache Commons Collections (commons-collections:commons-collections:3.2.2 - http://commons.apache.org/collections/) * Commons Digester (commons-digester:commons-digester:2.1 - http://commons.apache.org/digester/) - * Apache Commons IO (commons-io:commons-io:2.21.0 - https://commons.apache.org/proper/commons-io/) + * Apache Commons IO (commons-io:commons-io:2.22.0 - https://commons.apache.org/proper/commons-io/) * Commons Lang (commons-lang:commons-lang:2.6 - http://commons.apache.org/lang/) - * Apache Commons Logging (commons-logging:commons-logging:1.3.5 - https://commons.apache.org/proper/commons-logging/) + * Apache Commons Logging (commons-logging:commons-logging:1.3.6 - https://commons.apache.org/proper/commons-logging/) * Apache Commons Validator (commons-validator:commons-validator:1.10.1 - https://commons.apache.org/proper/commons-validator/) * GeoJson POJOs for Jackson (de.grundid.opendatalab:geojson-jackson:1.14 - https://github.com/opendatalab-de/geojson-jackson) * broker-client (eu.openaire:broker-client:1.1.2 - http://api.openaire.eu/broker/broker-client) @@ -107,10 +108,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Metrics Integration with JMX (io.dropwizard.metrics:metrics-jmx:4.1.5 - https://metrics.dropwizard.io/metrics-jmx) * JVM Integration for Metrics (io.dropwizard.metrics:metrics-jvm:4.1.5 - https://metrics.dropwizard.io/metrics-jvm) * SWORD v2 Common Server Library (forked) (io.gdcc:sword2-server:2.0.0 - https://github.com/gdcc/sword2-server) - * micrometer-commons (io.micrometer:micrometer-commons:1.14.13 - https://github.com/micrometer-metrics/micrometer) - * micrometer-core (io.micrometer:micrometer-core:1.15.6 - https://github.com/micrometer-metrics/micrometer) - * micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.15.6 - https://github.com/micrometer-metrics/micrometer) - * micrometer-observation (io.micrometer:micrometer-observation:1.14.13 - https://github.com/micrometer-metrics/micrometer) + * micrometer-commons (io.micrometer:micrometer-commons:1.15.11 - https://github.com/micrometer-metrics/micrometer) + * micrometer-core (io.micrometer:micrometer-core:1.15.11 - https://github.com/micrometer-metrics/micrometer) + * micrometer-jakarta9 (io.micrometer:micrometer-jakarta9:1.15.11 - https://github.com/micrometer-metrics/micrometer) + * micrometer-observation (io.micrometer:micrometer-observation:1.15.11 - https://github.com/micrometer-metrics/micrometer) * Netty/Buffer (io.netty:netty-buffer:4.1.99.Final - https://netty.io/netty-buffer/) * Netty/Codec (io.netty:netty-codec:4.1.99.Final - https://netty.io/netty-codec/) * Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.86.Final - https://netty.io/netty-codec-http/) @@ -163,7 +164,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Byte Buddy (without dependencies) (net.bytebuddy:byte-buddy:1.14.11 - https://bytebuddy.net/byte-buddy) * Byte Buddy agent (net.bytebuddy:byte-buddy-agent:1.11.13 - https://bytebuddy.net/byte-buddy-agent) * eigenbase-properties (net.hydromatic:eigenbase-properties:1.1.5 - http://github.com/julianhyde/eigenbase-properties) - * Java Native Access (net.java.dev.jna:jna:5.13.0 - https://github.com/java-native-access/jna) + * Java Native Access (net.java.dev.jna:jna:5.18.1 - https://github.com/java-native-access/jna) * json-unit-core (net.javacrumbs.json-unit:json-unit-core:2.36.0 - https://github.com/lukas-krecan/JsonUnit/json-unit-core) * "Java Concurrency in Practice" book annotations (net.jcip:jcip-annotations:1.0 - http://jcip.net/) * ASM based accessors helper used by json-smart (net.minidev:accessors-smart:2.6.0 - https://urielch.github.io/) @@ -171,25 +172,25 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Abdera Core (org.apache.abdera:abdera-core:1.1.3 - http://abdera.apache.org/abdera-core) * I18N Libraries (org.apache.abdera:abdera-i18n:1.1.3 - http://abdera.apache.org) * Abdera Parser (org.apache.abdera:abdera-parser:1.1.3 - http://abdera.apache.org/abdera-parser) - * Apache Ant Core (org.apache.ant:ant:1.10.15 - https://ant.apache.org/) - * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.15 - https://ant.apache.org/) - * Apache Commons BCEL (org.apache.bcel:bcel:6.11.0 - https://commons.apache.org/proper/commons-bcel) + * Apache Ant Core (org.apache.ant:ant:1.10.17 - https://ant.apache.org/) + * Apache Ant Launcher (org.apache.ant:ant-launcher:1.10.17 - https://ant.apache.org/) + * Apache Commons BCEL (org.apache.bcel:bcel:6.12.0 - https://commons.apache.org/proper/commons-bcel) * Calcite Core (org.apache.calcite:calcite-core:1.35.0 - https://calcite.apache.org) * Calcite Linq4j (org.apache.calcite:calcite-linq4j:1.35.0 - https://calcite.apache.org) * Apache Calcite Avatica (org.apache.calcite.avatica:avatica-core:1.23.0 - https://calcite.apache.org/avatica) * Apache Calcite Avatica Metrics (org.apache.calcite.avatica:avatica-metrics:1.23.0 - https://calcite.apache.org/avatica) * Apache Commons Collections (org.apache.commons:commons-collections4:4.5.0 - https://commons.apache.org/proper/commons-collections/) * Apache Commons Compress (org.apache.commons:commons-compress:1.28.0 - https://commons.apache.org/proper/commons-compress/) - * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.13.0 - https://commons.apache.org/proper/commons-configuration/) + * Apache Commons Configuration (org.apache.commons:commons-configuration2:2.15.0 - https://commons.apache.org/proper/commons-configuration/) * Apache Commons CSV (org.apache.commons:commons-csv:1.14.1 - https://commons.apache.org/proper/commons-csv/) - * Apache Commons DBCP (org.apache.commons:commons-dbcp2:2.13.0 - https://commons.apache.org/proper/commons-dbcp/) + * Apache Commons DBCP (org.apache.commons:commons-dbcp2:2.14.0 - https://commons.apache.org/proper/commons-dbcp/) * Apache Commons Digester (org.apache.commons:commons-digester3:3.2 - http://commons.apache.org/digester/) * Apache Commons Exec (org.apache.commons:commons-exec:1.3 - http://commons.apache.org/proper/commons-exec/) - * Apache Commons Exec (org.apache.commons:commons-exec:1.5.0 - https://commons.apache.org/proper/commons-exec/) + * Apache Commons Exec (org.apache.commons:commons-exec:1.6.0 - https://commons.apache.org/proper/commons-exec/) * Apache Commons Lang (org.apache.commons:commons-lang3:3.20.0 - https://commons.apache.org/proper/commons-lang/) * Apache Commons Math (org.apache.commons:commons-math3:3.6.1 - http://commons.apache.org/proper/commons-math/) - * Apache Commons Pool (org.apache.commons:commons-pool2:2.12.1 - https://commons.apache.org/proper/commons-pool/) - * Apache Commons Text (org.apache.commons:commons-text:1.14.0 - https://commons.apache.org/proper/commons-text) + * Apache Commons Pool (org.apache.commons:commons-pool2:2.13.1 - https://commons.apache.org/proper/commons-pool/) + * Apache Commons Text (org.apache.commons:commons-text:1.15.0 - https://commons.apache.org/proper/commons-text) * Curator Client (org.apache.curator:curator-client:2.13.0 - http://curator.apache.org/curator-client) * Curator Framework (org.apache.curator:curator-framework:2.13.0 - http://curator.apache.org/curator-framework) * Curator Recipes (org.apache.curator:curator-recipes:2.13.0 - http://curator.apache.org/curator-recipes) @@ -203,12 +204,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.16 - http://hc.apache.org/httpcomponents-core-ga) * Apache HttpClient Mime (org.apache.httpcomponents:httpmime:4.5.14 - http://hc.apache.org/httpcomponents-client-ga) * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.1.3 - https://hc.apache.org/httpcomponents-client-5.0.x/5.1.3/httpclient5/) - * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.5.1 - https://hc.apache.org/httpcomponents-client-5.5.x/5.5.1/httpclient5/) + * Apache HttpClient (org.apache.httpcomponents.client5:httpclient5:5.6.1 - https://hc.apache.org/httpcomponents-client-5.5.x/5.6.1/httpclient5/) * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5/) - * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.3.6 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.6/httpcore5/) + * Apache HttpComponents Core HTTP/1.1 (org.apache.httpcomponents.core5:httpcore5:5.4 - https://hc.apache.org/httpcomponents-core-5.4.x/5.4/httpcore5/) * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.1.3 - https://hc.apache.org/httpcomponents-core-5.1.x/5.1.3/httpcore5-h2/) - * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.3.6 - https://hc.apache.org/httpcomponents-core-5.3.x/5.3.6/httpcore5-h2/) - * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.13 - http://james.apache.org/mime4j/apache-mime4j-core) + * Apache HttpComponents Core HTTP/2 (org.apache.httpcomponents.core5:httpcore5-h2:5.4 - https://hc.apache.org/httpcomponents-core-5.4.x/5.4/httpcore5-h2/) + * Apache James :: Mime4j :: Core (org.apache.james:apache-mime4j-core:0.8.14 - http://james.apache.org/mime4j/apache-mime4j-core) * Apache James :: Mime4j :: DOM (org.apache.james:apache-mime4j-dom:0.8.13 - http://james.apache.org/mime4j/apache-mime4j-dom) * Apache Jena - Libraries POM (org.apache.jena:apache-jena-libs:4.10.0 - https://jena.apache.org/apache-jena-libs/) * Apache Jena - ARQ (org.apache.jena:jena-arq:4.10.0 - https://jena.apache.org/jena-arq/) @@ -231,12 +232,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Kerby ASN1 Project (org.apache.kerby:kerby-asn1:1.0.1 - http://directory.apache.org/kerby/kerby-common/kerby-asn1) * Kerby PKIX Project (org.apache.kerby:kerby-pkix:1.0.1 - http://directory.apache.org/kerby/kerby-pkix) * Apache Log4j 1.x Compatibility API (org.apache.logging.log4j:log4j-1.2-api:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-1.2-api/) - * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.2 - https://logging.apache.org/log4j/2.x/) - * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.2 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j API (org.apache.logging.log4j:log4j-api:2.25.4 - https://logging.apache.org/log4j/2.x/) + * Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.25.4 - https://logging.apache.org/log4j/2.x/) * Apache Log4j JUL Adapter (org.apache.logging.log4j:log4j-jul:2.24.3 - https://logging.apache.org/log4j/2.x/log4j/log4j-jul/) * Apache Log4j Layout for JSON template (org.apache.logging.log4j:log4j-layout-template-json:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-layout-template-json/) * Apache Log4j SLF4J Binding (org.apache.logging.log4j:log4j-slf4j-impl:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-slf4j-impl/) - * SLF4J 2 Provider for Log4j API (org.apache.logging.log4j:log4j-slf4j2-impl:2.25.2 - https://logging.apache.org/log4j/2.x/) + * SLF4J 2 Provider for Log4j API (org.apache.logging.log4j:log4j-slf4j2-impl:2.25.4 - https://logging.apache.org/log4j/2.x/) * Apache Log4j Web (org.apache.logging.log4j:log4j-web:2.17.2 - https://logging.apache.org/log4j/2.x/log4j-web/) * Lucene Common Analyzers (org.apache.lucene:lucene-analyzers-common:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-common) * Lucene ICU Analysis Components (org.apache.lucene:lucene-analyzers-icu:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-analyzers-icu) @@ -261,49 +262,49 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Lucene Spatial Extras (org.apache.lucene:lucene-spatial-extras:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial-extras) * Lucene Spatial 3D (org.apache.lucene:lucene-spatial3d:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-spatial3d) * Lucene Suggest (org.apache.lucene:lucene-suggest:8.11.4 - https://lucene.apache.org/lucene-parent/lucene-suggest) - * Apache FontBox (org.apache.pdfbox:fontbox:3.0.5 - http://pdfbox.apache.org/) + * Apache FontBox (org.apache.pdfbox:fontbox:3.0.7 - http://pdfbox.apache.org/) * PDFBox JBIG2 ImageIO plugin (org.apache.pdfbox:jbig2-imageio:3.0.4 - https://www.apache.org/jbig2-imageio/) * Apache JempBox (org.apache.pdfbox:jempbox:1.8.17 - http://www.apache.org/pdfbox-parent/jempbox/) - * Apache PDFBox (org.apache.pdfbox:pdfbox:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox/) - * Apache PDFBox io (org.apache.pdfbox:pdfbox-io:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox-io/) - * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:3.0.5 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) - * Apache XmpBox (org.apache.pdfbox:xmpbox:3.0.5 - https://www.apache.org/pdfbox-parent/xmpbox/) - * Apache POI - Common (org.apache.poi:poi:5.4.1 - https://poi.apache.org/) - * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.4.1 - https://poi.apache.org/) - * Apache POI (org.apache.poi:poi-ooxml-lite:5.4.1 - https://poi.apache.org/) - * Apache POI (org.apache.poi:poi-scratchpad:5.4.1 - https://poi.apache.org/) + * Apache PDFBox (org.apache.pdfbox:pdfbox:3.0.7 - https://www.apache.org/pdfbox-parent/pdfbox/) + * Apache PDFBox io (org.apache.pdfbox:pdfbox-io:3.0.7 - https://www.apache.org/pdfbox-parent/pdfbox-io/) + * Apache PDFBox tools (org.apache.pdfbox:pdfbox-tools:3.0.7 - https://www.apache.org/pdfbox-parent/pdfbox-tools/) + * Apache XmpBox (org.apache.pdfbox:xmpbox:3.0.7 - https://www.apache.org/pdfbox-parent/xmpbox/) + * Apache POI - Common (org.apache.poi:poi:5.5.1 - https://poi.apache.org/) + * Apache POI - API based on OPC and OOXML schemas (org.apache.poi:poi-ooxml:5.5.1 - https://poi.apache.org/) + * Apache POI - OOXML schemas (full) (org.apache.poi:poi-ooxml-full:5.5.1 - https://poi.apache.org/) + * Apache POI (org.apache.poi:poi-scratchpad:5.5.1 - https://poi.apache.org/) * Apache Solr Core (org.apache.solr:solr-core:8.11.4 - https://lucene.apache.org/solr-parent/solr-core) * Apache Solr Solrj (org.apache.solr:solr-solrj:8.11.4 - https://lucene.apache.org/solr-parent/solr-solrj) * Apache Standard Taglib Implementation (org.apache.taglibs:taglibs-standard-impl:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-impl) * Apache Standard Taglib Specification API (org.apache.taglibs:taglibs-standard-spec:1.2.5 - http://tomcat.apache.org/taglibs/standard-1.2.5/taglibs-standard-spec) * Apache Thrift (org.apache.thrift:libthrift:0.19.0 - http://thrift.apache.org) - * Apache Tika core (org.apache.tika:tika-core:3.2.3 - https://tika.apache.org/) - * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:3.2.3 - https://tika.apache.org/tika-parser-apple-module/) - * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:3.2.3 - https://tika.apache.org/tika-parser-audiovideo-module/) - * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:3.2.3 - https://tika.apache.org/tika-parser-cad-module/) - * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:3.2.3 - https://tika.apache.org/tika-parser-code-module/) - * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:3.2.3 - https://tika.apache.org/tika-parser-crypto-module/) - * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:3.2.3 - https://tika.apache.org/tika-parser-digest-commons/) - * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:3.2.3 - https://tika.apache.org/tika-parser-font-module/) - * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:3.2.3 - https://tika.apache.org/tika-parser-html-module/) - * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:3.2.3 - https://tika.apache.org/tika-parser-image-module/) - * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:3.2.3 - https://tika.apache.org/tika-parser-mail-commons/) - * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:3.2.3 - https://tika.apache.org/tika-parser-mail-module/) - * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:3.2.3 - https://tika.apache.org/tika-parser-microsoft-module/) - * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:3.2.3 - https://tika.apache.org/tika-parser-miscoffice-module/) - * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:3.2.3 - https://tika.apache.org/tika-parser-news-module/) - * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:3.2.3 - https://tika.apache.org/tika-parser-ocr-module/) - * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:3.2.3 - https://tika.apache.org/tika-parser-pdf-module/) - * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:3.2.3 - https://tika.apache.org/tika-parser-pkg-module/) - * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:3.2.3 - https://tika.apache.org/tika-parser-text-module/) - * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:3.2.3 - https://tika.apache.org/tika-parser-webarchive-module/) - * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:3.2.3 - https://tika.apache.org/tika-parser-xml-module/) - * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:3.2.3 - https://tika.apache.org/tika-parser-xmp-commons/) - * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:3.2.3 - https://tika.apache.org/tika-parser-zip-commons/) - * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:3.2.3 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) - * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:10.1.49 - https://tomcat.apache.org/) - * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:10.1.49 - https://tomcat.apache.org/) - * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:10.1.49 - https://tomcat.apache.org/) + * Apache Tika core (org.apache.tika:tika-core:3.3.0 - https://tika.apache.org/) + * Apache Tika Apple parser module (org.apache.tika:tika-parser-apple-module:3.3.0 - https://tika.apache.org/tika-parser-apple-module/) + * Apache Tika audiovideo parser module (org.apache.tika:tika-parser-audiovideo-module:3.3.0 - https://tika.apache.org/tika-parser-audiovideo-module/) + * Apache Tika cad parser module (org.apache.tika:tika-parser-cad-module:3.3.0 - https://tika.apache.org/tika-parser-cad-module/) + * Apache Tika code parser module (org.apache.tika:tika-parser-code-module:3.3.0 - https://tika.apache.org/tika-parser-code-module/) + * Apache Tika crypto parser module (org.apache.tika:tika-parser-crypto-module:3.3.0 - https://tika.apache.org/tika-parser-crypto-module/) + * Apache Tika digest commons (org.apache.tika:tika-parser-digest-commons:3.3.0 - https://tika.apache.org/tika-parser-digest-commons/) + * Apache Tika font parser module (org.apache.tika:tika-parser-font-module:3.3.0 - https://tika.apache.org/tika-parser-font-module/) + * Apache Tika html parser module (org.apache.tika:tika-parser-html-module:3.3.0 - https://tika.apache.org/tika-parser-html-module/) + * Apache Tika image parser module (org.apache.tika:tika-parser-image-module:3.3.0 - https://tika.apache.org/tika-parser-image-module/) + * Apache Tika mail commons (org.apache.tika:tika-parser-mail-commons:3.3.0 - https://tika.apache.org/tika-parser-mail-commons/) + * Apache Tika mail parser module (org.apache.tika:tika-parser-mail-module:3.3.0 - https://tika.apache.org/tika-parser-mail-module/) + * Apache Tika Microsoft parser module (org.apache.tika:tika-parser-microsoft-module:3.3.0 - https://tika.apache.org/tika-parser-microsoft-module/) + * Apache Tika miscellaneous office format parser module (org.apache.tika:tika-parser-miscoffice-module:3.3.0 - https://tika.apache.org/tika-parser-miscoffice-module/) + * Apache Tika news parser module (org.apache.tika:tika-parser-news-module:3.3.0 - https://tika.apache.org/tika-parser-news-module/) + * Apache Tika OCR parser module (org.apache.tika:tika-parser-ocr-module:3.3.0 - https://tika.apache.org/tika-parser-ocr-module/) + * Apache Tika PDF parser module (org.apache.tika:tika-parser-pdf-module:3.3.0 - https://tika.apache.org/tika-parser-pdf-module/) + * Apache Tika package parser module (org.apache.tika:tika-parser-pkg-module:3.3.0 - https://tika.apache.org/tika-parser-pkg-module/) + * Apache Tika text parser module (org.apache.tika:tika-parser-text-module:3.3.0 - https://tika.apache.org/tika-parser-text-module/) + * Apache Tika WARC parser module (org.apache.tika:tika-parser-webarchive-module:3.3.0 - https://tika.apache.org/tika-parser-webarchive-module/) + * Apache Tika XML parser module (org.apache.tika:tika-parser-xml-module:3.3.0 - https://tika.apache.org/tika-parser-xml-module/) + * Apache Tika XMP commons (org.apache.tika:tika-parser-xmp-commons:3.3.0 - https://tika.apache.org/tika-parser-xmp-commons/) + * Apache Tika ZIP commons (org.apache.tika:tika-parser-zip-commons:3.3.0 - https://tika.apache.org/tika-parser-zip-commons/) + * Apache Tika standard parser package (org.apache.tika:tika-parsers-standard-package:3.3.0 - https://tika.apache.org/tika-parsers/tika-parsers-standard/tika-parsers-standard-package/) + * tomcat-embed-core (org.apache.tomcat.embed:tomcat-embed-core:10.1.54 - https://tomcat.apache.org/) + * tomcat-embed-el (org.apache.tomcat.embed:tomcat-embed-el:10.1.54 - https://tomcat.apache.org/) + * tomcat-embed-websocket (org.apache.tomcat.embed:tomcat-embed-websocket:10.1.54 - https://tomcat.apache.org/) * Apache Velocity - Engine (org.apache.velocity:velocity-engine-core:2.4.1 - http://velocity.apache.org/engine/devel/velocity-engine-core/) * Apache Velocity - JSR 223 Scripting (org.apache.velocity:velocity-engine-scripting:2.3 - http://velocity.apache.org/engine/devel/velocity-engine-scripting/) * Apache Velocity Tools - Generic tools (org.apache.velocity.tools:velocity-tools-generic:3.1 - https://velocity.apache.org/tools/devel/velocity-tools-generic/) @@ -313,7 +314,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Apache ZooKeeper - Server (org.apache.zookeeper:zookeeper:3.6.2 - http://zookeeper.apache.org/zookeeper) * Apache ZooKeeper - Jute (org.apache.zookeeper:zookeeper-jute:3.6.2 - http://zookeeper.apache.org/zookeeper-jute) * org.apiguardian:apiguardian-api (org.apiguardian:apiguardian-api:1.1.2 - https://github.com/apiguardian-team/apiguardian) - * AssertJ Core (org.assertj:assertj-core:3.27.6 - https://assertj.github.io/doc/#assertj-core) + * AssertJ Core (org.assertj:assertj-core:3.27.7 - https://assertj.github.io/doc/#assertj-core) * Evo Inflector (org.atteo:evo-inflector:1.3 - http://atteo.org/static/evo-inflector) * attoparser (org.attoparser:attoparser:2.0.7.RELEASE - https://www.attoparser.org) * Awaitility (org.awaitility:awaitility:4.2.2 - http://awaitility.org) @@ -359,7 +360,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.15.v20190215 - https://eclipse.org/jetty/http2-parent/http2-server) * Jetty :: HTTP2 :: Server (org.eclipse.jetty.http2:http2-server:9.4.53.v20231009 - https://eclipse.org/jetty/http2-parent/http2-server) * Jetty :: Schemas (org.eclipse.jetty.toolchain:jetty-schemas:3.1.2 - https://eclipse.org/jetty/jetty-schemas) - * Ehcache (org.ehcache:ehcache:3.11.1 - http://ehcache.org) + * Ehcache (org.ehcache:ehcache:3.12.0 - http://ehcache.org) * flyway-core (org.flywaydb:flyway-core:10.22.0 - https://flywaydb.org/flyway-core) * flyway-database-postgresql (org.flywaydb:flyway-database-postgresql:10.22.0 - https://flywaydb.org/flyway-database-postgresql) * Ogg and Vorbis for Java, Core (org.gagravarr:vorbis-java-core:0.8 - https://github.com/Gagravarr/VorbisJava) @@ -372,7 +373,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Hibernate Validator Portable Extension (org.hibernate.validator:hibernate-validator-cdi:8.0.3.Final - https://hibernate.org/validator) * org.immutables.value-annotations (org.immutables:value-annotations:2.9.2 - http://immutables.org/value-annotations) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) - * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.6.1.Final - http://www.jboss.org) + * JBoss Logging 3 (org.jboss.logging:jboss-logging:3.5.0.Final - http://www.jboss.org) * JDOM (org.jdom:jdom2:2.0.6.1 - http://www.jdom.org) * JetBrains Java Annotations (org.jetbrains:annotations:17.0.0 - https://github.com/JetBrains/java-annotations) * Kotlin Stdlib (org.jetbrains.kotlin:kotlin-stdlib:1.8.21 - https://kotlinlang.org/) @@ -386,56 +387,55 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * MockServer Core (org.mock-server:mockserver-core:5.15.0 - https://www.mock-server.com) * MockServer JUnit 4 Integration (org.mock-server:mockserver-junit-rule:5.15.0 - https://www.mock-server.com) * MockServer & Proxy Netty (org.mock-server:mockserver-netty:5.15.0 - https://www.mock-server.com) - * jwarc (org.netpreserve:jwarc:0.32.0 - https://github.com/iipc/jwarc) + * jwarc (org.netpreserve:jwarc:0.35.0 - https://github.com/iipc/jwarc) * Objenesis (org.objenesis:objenesis:3.2 - http://objenesis.org/objenesis) * org.roaringbitmap:RoaringBitmap (org.roaringbitmap:RoaringBitmap:1.0.0 - https://github.com/RoaringBitmap/RoaringBitmap) * RRD4J (org.rrd4j:rrd4j:3.5 - https://github.com/rrd4j/rrd4j/) * JSONassert (org.skyscreamer:jsonassert:1.5.3 - https://github.com/skyscreamer/JSONassert) * JCL 1.2 implemented over SLF4J (org.slf4j:jcl-over-slf4j:2.0.17 - http://www.slf4j.org) - * Spring AOP (org.springframework:spring-aop:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Beans (org.springframework:spring-beans:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Context (org.springframework:spring-context:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Context Support (org.springframework:spring-context-support:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Core (org.springframework:spring-core:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Commons Logging Bridge (org.springframework:spring-jcl:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring JDBC (org.springframework:spring-jdbc:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring LDAP Core (org.springframework.ldap:spring-ldap-core:3.2.15 - https://github.com/spring-projects/spring-ldap) - * Spring Object/Relational Mapping (org.springframework:spring-orm:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring TestContext Framework (org.springframework:spring-test:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Transaction (org.springframework:spring-tx:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Web (org.springframework:spring-web:6.2.14 - https://github.com/spring-projects/spring-framework) - * Spring Web MVC (org.springframework:spring-webmvc:6.2.14 - https://github.com/spring-projects/spring-framework) - * spring-boot (org.springframework.boot:spring-boot:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter (org.springframework.boot:spring-boot-starter:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-thymeleaf (org.springframework.boot:spring-boot-starter-thymeleaf:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-test (org.springframework.boot:spring-boot-test:3.5.8 - https://spring.io/projects/spring-boot) - * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:3.5.8 - https://spring.io/projects/spring-boot) - * Spring Data Core (org.springframework.data:spring-data-commons:3.5.6 - https://spring.io/projects/spring-data) - * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:4.5.6 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-core) - * Spring Data REST - WebMVC (org.springframework.data:spring-data-rest-webmvc:4.5.6 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc) - * Spring HATEOAS (org.springframework.hateoas:spring-hateoas:2.5.1 - https://github.com/spring-projects/spring-hateoas) + * Spring AOP (org.springframework:spring-aop:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Beans (org.springframework:spring-beans:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Context (org.springframework:spring-context:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Context Support (org.springframework:spring-context-support:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Core (org.springframework:spring-core:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Expression Language (SpEL) (org.springframework:spring-expression:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring JDBC (org.springframework:spring-jdbc:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Object/Relational Mapping (org.springframework:spring-orm:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring TestContext Framework (org.springframework:spring-test:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Transaction (org.springframework:spring-tx:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Web (org.springframework:spring-web:6.2.18 - https://github.com/spring-projects/spring-framework) + * Spring Web MVC (org.springframework:spring-webmvc:6.2.18 - https://github.com/spring-projects/spring-framework) + * spring-boot (org.springframework.boot:spring-boot:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-actuator (org.springframework.boot:spring-boot-actuator:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-actuator-autoconfigure (org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-autoconfigure (org.springframework.boot:spring-boot-autoconfigure:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter (org.springframework.boot:spring-boot-starter:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-actuator (org.springframework.boot:spring-boot-starter-actuator:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-aop (org.springframework.boot:spring-boot-starter-aop:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-cache (org.springframework.boot:spring-boot-starter-cache:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-data-rest (org.springframework.boot:spring-boot-starter-data-rest:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-json (org.springframework.boot:spring-boot-starter-json:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-log4j2 (org.springframework.boot:spring-boot-starter-log4j2:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-security (org.springframework.boot:spring-boot-starter-security:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-test (org.springframework.boot:spring-boot-starter-test:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-thymeleaf (org.springframework.boot:spring-boot-starter-thymeleaf:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-tomcat (org.springframework.boot:spring-boot-starter-tomcat:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-starter-web (org.springframework.boot:spring-boot-starter-web:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-test (org.springframework.boot:spring-boot-test:3.5.14 - https://spring.io/projects/spring-boot) + * spring-boot-test-autoconfigure (org.springframework.boot:spring-boot-test-autoconfigure:3.5.14 - https://spring.io/projects/spring-boot) + * Spring Data Core (org.springframework.data:spring-data-commons:3.5.11 - https://spring.io/projects/spring-data) + * Spring Data REST - Core (org.springframework.data:spring-data-rest-core:4.5.11 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-core) + * Spring Data REST - WebMVC (org.springframework.data:spring-data-rest-webmvc:4.5.11 - https://www.spring.io/spring-data/spring-data-rest-parent/spring-data-rest-webmvc) + * Spring HATEOAS (org.springframework.hateoas:spring-hateoas:2.5.2 - https://github.com/spring-projects/spring-hateoas) + * spring-ldap-core (org.springframework.ldap:spring-ldap-core:3.3.7 - https://spring.io/projects/spring-ldap) * Spring Plugin - Core (org.springframework.plugin:spring-plugin-core:3.0.0 - https://github.com/spring-projects/spring-plugin/spring-plugin-core) - * spring-security-config (org.springframework.security:spring-security-config:6.5.7 - https://spring.io/projects/spring-security) - * spring-security-core (org.springframework.security:spring-security-core:6.5.7 - https://spring.io/projects/spring-security) - * spring-security-crypto (org.springframework.security:spring-security-crypto:6.5.7 - https://spring.io/projects/spring-security) - * spring-security-test (org.springframework.security:spring-security-test:6.5.7 - https://spring.io/projects/spring-security) - * spring-security-web (org.springframework.security:spring-security-web:6.5.7 - https://spring.io/projects/spring-security) - * thymeleaf (org.thymeleaf:thymeleaf:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf) - * thymeleaf-spring6 (org.thymeleaf:thymeleaf-spring6:3.1.3.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf-spring6) + * spring-security-config (org.springframework.security:spring-security-config:6.5.10 - https://spring.io/projects/spring-security) + * spring-security-core (org.springframework.security:spring-security-core:6.5.10 - https://spring.io/projects/spring-security) + * spring-security-crypto (org.springframework.security:spring-security-crypto:6.5.10 - https://spring.io/projects/spring-security) + * spring-security-test (org.springframework.security:spring-security-test:6.5.10 - https://spring.io/projects/spring-security) + * spring-security-web (org.springframework.security:spring-security-web:6.5.10 - https://spring.io/projects/spring-security) + * thymeleaf (org.thymeleaf:thymeleaf:3.1.5.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf) + * thymeleaf-spring6 (org.thymeleaf:thymeleaf-spring6:3.1.5.RELEASE - http://www.thymeleaf.org/thymeleaf-lib/thymeleaf-spring6) * unbescape (org.unbescape:unbescape:1.1.6.RELEASE - http://www.unbescape.org) * snappy-java (org.xerial.snappy:snappy-java:1.1.10.1 - https://github.com/xerial/snappy-java) * xml-matchers (org.xmlmatchers:xml-matchers:0.10 - http://code.google.com/p/xml-matchers/) @@ -443,38 +443,42 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * org.xmlunit:xmlunit-core (org.xmlunit:xmlunit-core:2.11.0 - https://www.xmlunit.org/) * org.xmlunit:xmlunit-placeholders (org.xmlunit:xmlunit-placeholders:2.9.1 - https://www.xmlunit.org/xmlunit-placeholders/) * SnakeYAML (org.yaml:snakeyaml:2.4 - https://bitbucket.org/snakeyaml/snakeyaml) - * AWS Java SDK :: Annotations (software.amazon.awssdk:annotations:2.38.8 - https://aws.amazon.com/sdkforjava/core/annotations) - * AWS Java SDK :: Arns (software.amazon.awssdk:arns:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Auth (software.amazon.awssdk:auth:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: AWS Core (software.amazon.awssdk:aws-core:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Core :: Protocols :: AWS Query Protocol (software.amazon.awssdk:aws-query-protocol:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Core :: Protocols :: AWS Xml Protocol (software.amazon.awssdk:aws-xml-protocol:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Checksums (software.amazon.awssdk:checksums:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Checksums SPI (software.amazon.awssdk:checksums-spi:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: AWS CRT Core (software.amazon.awssdk:crt-core:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Endpoints SPI (software.amazon.awssdk:endpoints-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/endpoints-spi) - * AWS Java SDK :: HTTP Auth (software.amazon.awssdk:http-auth:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: HTTP Auth AWS (software.amazon.awssdk:http-auth-aws:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: HTTP Auth Event Stream (software.amazon.awssdk:http-auth-aws-eventstream:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: HTTP Auth SPI (software.amazon.awssdk:http-auth-spi:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: HTTP Client Interface (software.amazon.awssdk:http-client-spi:2.38.8 - https://aws.amazon.com/sdkforjava/http-client-spi) - * AWS Java SDK :: Identity SPI (software.amazon.awssdk:identity-spi:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Core :: Protocols :: Json Utils (software.amazon.awssdk:json-utils:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Metrics SPI (software.amazon.awssdk:metrics-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/metrics-spi) - * AWS Java SDK :: Profiles (software.amazon.awssdk:profiles:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Core :: Protocols :: Protocol Core (software.amazon.awssdk:protocol-core:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Regions (software.amazon.awssdk:regions:2.38.8 - https://aws.amazon.com/sdkforjava/core/regions) - * AWS Java SDK :: Retries (software.amazon.awssdk:retries:2.38.8 - https://aws.amazon.com/sdkforjava/core/retries) - * AWS Java SDK :: Retries API (software.amazon.awssdk:retries-spi:2.38.8 - https://aws.amazon.com/sdkforjava/core/retries-spi) - * AWS Java SDK :: Services :: Amazon S3 (software.amazon.awssdk:s3:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: SDK Core (software.amazon.awssdk:sdk-core:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Third Party :: Jackson-core (software.amazon.awssdk:third-party-jackson-core:2.38.8 - https://aws.amazon.com/sdkforjava) - * AWS Java SDK :: Utilities (software.amazon.awssdk:utils:2.38.8 - https://aws.amazon.com/sdkforjava/utils) - * AWS Java SDK :: Utils Lite (software.amazon.awssdk:utils-lite:2.38.8 - https://aws.amazon.com/sdkforjava) - * software.amazon.awssdk.crt:aws-crt (software.amazon.awssdk.crt:aws-crt:0.39.4 - https://github.com/awslabs/aws-crt-java) + * AWS Java SDK :: Annotations (software.amazon.awssdk:annotations:2.43.2 - https://aws.amazon.com/sdkforjava/core/annotations) + * AWS Java SDK :: Arns (software.amazon.awssdk:arns:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Auth (software.amazon.awssdk:auth:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: AWS Core (software.amazon.awssdk:aws-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: AWS Query Protocol (software.amazon.awssdk:aws-query-protocol:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: AWS Xml Protocol (software.amazon.awssdk:aws-xml-protocol:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Checksums (software.amazon.awssdk:checksums:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Checksums SPI (software.amazon.awssdk:checksums-spi:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: AWS CRT Core (software.amazon.awssdk:crt-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Endpoints SPI (software.amazon.awssdk:endpoints-spi:2.43.2 - https://aws.amazon.com/sdkforjava/core/endpoints-spi) + * AWS Java SDK :: HTTP Auth (software.amazon.awssdk:http-auth:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth AWS (software.amazon.awssdk:http-auth-aws:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth Event Stream (software.amazon.awssdk:http-auth-aws-eventstream:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Auth SPI (software.amazon.awssdk:http-auth-spi:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: HTTP Client Interface (software.amazon.awssdk:http-client-spi:2.43.2 - https://aws.amazon.com/sdkforjava/http-client-spi) + * AWS Java SDK :: Identity SPI (software.amazon.awssdk:identity-spi:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: Json Utils (software.amazon.awssdk:json-utils:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Metrics SPI (software.amazon.awssdk:metrics-spi:2.43.2 - https://aws.amazon.com/sdkforjava/core/metrics-spi) + * AWS Java SDK :: Profiles (software.amazon.awssdk:profiles:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Core :: Protocols :: Protocol Core (software.amazon.awssdk:protocol-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Regions (software.amazon.awssdk:regions:2.43.2 - https://aws.amazon.com/sdkforjava/core/regions) + * AWS Java SDK :: Retries (software.amazon.awssdk:retries:2.43.2 - https://aws.amazon.com/sdkforjava/core/retries) + * AWS Java SDK :: Retries API (software.amazon.awssdk:retries-spi:2.43.2 - https://aws.amazon.com/sdkforjava/core/retries-spi) + * AWS Java SDK :: Services :: Amazon S3 (software.amazon.awssdk:s3:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: SDK Core (software.amazon.awssdk:sdk-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Third Party :: Jackson-core (software.amazon.awssdk:third-party-jackson-core:2.43.2 - https://aws.amazon.com/sdkforjava) + * AWS Java SDK :: Utilities (software.amazon.awssdk:utils:2.43.2 - https://aws.amazon.com/sdkforjava/utils) + * AWS Java SDK :: Utils Lite (software.amazon.awssdk:utils-lite:2.43.2 - https://aws.amazon.com/sdkforjava) + * software.amazon.awssdk.crt:aws-crt (software.amazon.awssdk.crt:aws-crt:0.45.2 - https://github.com/awslabs/aws-crt-java) * AWS Event Stream (software.amazon.eventstream:eventstream:1.0.1 - https://github.com/awslabs/aws-eventstream-java) * Xerces2-j (xerces:xercesImpl:2.12.2 - https://xerces.apache.org/xerces2-j/) + BSD 2-Clause License: + + * zstd-jni (com.github.luben:zstd-jni:1.5.7-4 - https://github.com/luben/zstd-jni) + BSD License: * Adobe XMPCore (com.adobe.xmp:xmpcore:6.1.11 - https://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html) @@ -486,8 +490,8 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Protocol Buffers [Core] (com.google.protobuf:protobuf-java:3.24.3 - https://developers.google.com/protocol-buffers/protobuf-java/) * JZlib (com.jcraft:jzlib:1.1.3 - http://www.jcraft.com/jzlib/) * jmustache (com.samskivert:jmustache:1.15 - http://github.com/samskivert/jmustache) - * dnsjava (dnsjava:dnsjava:3.6.3 - https://github.com/dnsjava/dnsjava) - * jaxen (jaxen:jaxen:2.0.0 - http://www.cafeconleche.org/jaxen/jaxen) + * dnsjava (dnsjava:dnsjava:3.6.4 - https://github.com/dnsjava/dnsjava) + * jaxen (jaxen:jaxen:2.0.1 - https://jaxen-xpath.github.io/jaxen/jaxen/) * ANTLR 4 Runtime (org.antlr:antlr4-runtime:4.13.2 - https://www.antlr.org/antlr4-runtime/) * commons-compiler (org.codehaus.janino:commons-compiler:3.1.8 - http://janino-compiler.github.io/commons-compiler/) * janino (org.codehaus.janino:janino:3.1.8 - http://janino-compiler.github.io/janino/) @@ -504,10 +508,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * asm-analysis (org.ow2.asm:asm-analysis:8.0.1 - http://asm.ow2.io/) * asm-commons (org.ow2.asm:asm-commons:8.0.1 - http://asm.ow2.io/) * asm-tree (org.ow2.asm:asm-tree:8.0.1 - http://asm.ow2.io/) - * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.8 - https://jdbc.postgresql.org) + * PostgreSQL JDBC Driver (org.postgresql:postgresql:42.7.11 - https://jdbc.postgresql.org) * Reflections (org.reflections:reflections:0.9.12 - http://github.com/ronmamo/reflections) * JMatIO (org.tallison:jmatio:1.5 - https://github.com/tballison/jmatio) - * XZ for Java (org.tukaani:xz:1.10 - https://tukaani.org/xz/java.html) + * XZ for Java (org.tukaani:xz:1.12 - https://tukaani.org/xz/java.html) * XMLUnit for Java (xmlunit:xmlunit:1.3 - http://xmlunit.sourceforge.net/) Common Development and Distribution License (CDDL): @@ -520,11 +524,10 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jakarta Servlet (jakarta.servlet:jakarta.servlet-api:6.1.0 - https://projects.eclipse.org/projects/ee4j.servlet) * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta) * JavaBeans Activation Framework API jar (javax.activation:javax.activation-api:1.2.0 - http://java.net/all/javax.activation-api/) - * javax.annotation API (javax.annotation:javax.annotation-api:1.3 - http://jcp.org/en/jsr/detail?id=250) * Java Servlet API (javax.servlet:javax.servlet-api:3.1.0 - http://servlet-spec.java.net) * javax.transaction API (javax.transaction:javax.transaction-api:1.3 - http://jta-spec.java.net) * jaxb-api (javax.xml.bind:jaxb-api:2.3.1 - https://github.com/javaee/jaxb-spec/jaxb-api) - * JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight) + * JHighlight (org.codelibs:jhighlight:1.1.1 - https://github.com/codelibs/jhighlight) * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) * HK2 API module (org.glassfish.hk2:hk2-api:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-api) * ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:3.0.6 - https://github.com/eclipse-ee4j/glassfish-hk2/hk2-locator) @@ -550,12 +553,12 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * Jakarta Activation API (jakarta.activation:jakarta.activation-api:2.1.4 - https://github.com/jakartaee/jaf-api) * Jakarta Mail API (jakarta.mail:jakarta.mail-api:2.1.5 - https://projects.eclipse.org/projects/ee4j/jakarta.mail-api) * Jakarta Persistence API (jakarta.persistence:jakarta.persistence-api:3.1.0 - https://github.com/eclipse-ee4j/jpa-api) - * Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.4 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) + * Jakarta XML Binding API (jakarta.xml.bind:jakarta.xml.bind-api:4.0.5 - https://github.com/jakartaee/jaxb-api/jakarta.xml.bind-api) * Angus Activation Registries (org.eclipse.angus:angus-activation:2.0.3 - https://github.com/eclipse-ee4j/angus-activation/angus-activation) * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) - * JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.6 - https://eclipse-ee4j.github.io/jaxb-ri/) - * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.6 - https://eclipse-ee4j.github.io/jaxb-ri/) - * TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.6 - https://eclipse-ee4j.github.io/jaxb-ri/) + * JAXB Core (org.glassfish.jaxb:jaxb-core:4.0.8 - https://eclipse-ee4j.github.io/jaxb-ri/) + * JAXB Runtime (org.glassfish.jaxb:jaxb-runtime:4.0.8 - https://eclipse-ee4j.github.io/jaxb-ri/) + * TXW2 Runtime (org.glassfish.jaxb:txw2:4.0.8 - https://eclipse-ee4j.github.io/jaxb-ri/) * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) @@ -574,7 +577,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * jakarta.transaction API (jakarta.transaction:jakarta.transaction-api:2.0.1 - https://projects.eclipse.org/projects/ee4j.jta) * Jakarta RESTful WS API (jakarta.ws.rs:jakarta.ws.rs-api:3.1.0 - https://github.com/eclipse-ee4j/jaxrs-api) * JUnit (junit:junit:4.13.2 - http://junit.org) - * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.25 - https://www.eclipse.org/aspectj/) + * AspectJ Weaver (org.aspectj:aspectjweaver:1.9.25.1 - https://www.eclipse.org/aspectj/) * Angus Mail default provider (org.eclipse.angus:jakarta.mail:2.0.5 - http://eclipse-ee4j.github.io/angus-mail/jakarta.mail) * Jetty :: Apache JSP Implementation (org.eclipse.jetty:apache-jsp:9.4.15.v20190215 - http://www.eclipse.org/jetty) * Apache :: JSTL module (org.eclipse.jetty:apache-jstl:9.4.15.v20190215 - http://tomcat.apache.org/taglibs/standard/) @@ -646,14 +649,14 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * msg-simple (com.github.java-json-tools:msg-simple:1.2 - https://github.com/java-json-tools/msg-simple) * uri-template (com.github.java-json-tools:uri-template:0.10 - https://github.com/java-json-tools/uri-template) * FindBugs-Annotations (com.google.code.findbugs:annotations:3.0.1u2 - http://findbugs.sourceforge.net/) - * JHighlight (org.codelibs:jhighlight:1.1.0 - https://github.com/codelibs/jhighlight) + * JHighlight (org.codelibs:jhighlight:1.1.1 - https://github.com/codelibs/jhighlight) * Hibernate Commons Annotations (org.hibernate.common:hibernate-commons-annotations:6.0.6.Final - http://hibernate.org) * Hibernate ORM - hibernate-core (org.hibernate.orm:hibernate-core:6.4.10.Final - https://hibernate.org/orm) * Hibernate ORM - hibernate-jcache (org.hibernate.orm:hibernate-jcache:6.4.10.Final - https://hibernate.org/orm) * Hibernate ORM - hibernate-jpamodelgen (org.hibernate.orm:hibernate-jpamodelgen:6.4.10.Final - https://hibernate.org/orm) * im4java (org.im4java:im4java:1.4.0 - http://sourceforge.net/projects/im4java/) * Javassist (org.javassist:javassist:3.30.2-GA - https://www.javassist.org/) - * XOM (xom:xom:1.3.9 - https://xom.nu) + * XOM (xom:xom:1.4.1 - https://xom.nu) Go License: @@ -669,31 +672,32 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines LGPL-2.1-or-later: - * Java Native Access (net.java.dev.jna:jna:5.13.0 - https://github.com/java-native-access/jna) + * Java Native Access (net.java.dev.jna:jna:5.18.1 - https://github.com/java-native-access/jna) MIT License: * dexx (com.github.andrewoma.dexx:collection:0.7 - https://github.com/andrewoma/dexx) * Java SemVer (com.github.zafarkhaja:java-semver:0.9.0 - https://github.com/zafarkhaja/jsemver) - * dd-plist (com.googlecode.plist:dd-plist:1.28 - http://www.github.com/3breadt/dd-plist) + * dd-plist (com.googlecode.plist:dd-plist:1.29 - http://www.github.com/3breadt/dd-plist) * DigitalCollections: IIIF API Library (de.digitalcollections.iiif:iiif-apis:0.3.11 - https://github.com/dbmdz/iiif-apis) * ClassGraph (io.github.classgraph:classgraph:4.8.165 - https://github.com/classgraph/classgraph) * JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.4 - http://jopt-simple.github.io/jopt-simple) - * Bouncy Castle JavaMail Jakarta S/MIME APIs (org.bouncycastle:bcjmail-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) - * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.82 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle JavaMail Jakarta S/MIME APIs (org.bouncycastle:bcjmail-jdk18on:1.83 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle PKIX, CMS, EAC, TSP, PKCS, OCSP, CMP, and CRMF APIs (org.bouncycastle:bcpkix-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle Provider (org.bouncycastle:bcprov-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) + * Bouncy Castle ASN.1 Extension and Utility APIs (org.bouncycastle:bcutil-jdk18on:1.81 - https://www.bouncycastle.org/download/bouncy-castle-java/) * org.brotli:dec (org.brotli:dec:0.1.2 - http://brotli.org/dec) - * Checker Qual (org.checkerframework:checker-qual:3.52.0 - https://checkerframework.org/) + * Checker Qual (org.checkerframework:checker-qual:3.23.0 - https://checkerframework.org) + * Checker Qual (org.checkerframework:checker-qual:3.37.0 - https://checkerframework.org/) * jersey-core-client (org.glassfish.jersey.core:jersey-client:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/jersey-client) * jersey-inject-hk2 (org.glassfish.jersey.inject:jersey-hk2:3.1.11 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-hk2) * jersey-media-multipart (org.glassfish.jersey.media:jersey-media-multipart:3.1.3 - https://projects.eclipse.org/projects/ee4j.jersey/project/jersey-media-multipart) - * jsoup Java HTML Parser (org.jsoup:jsoup:1.21.2 - https://jsoup.org/) + * jsoup Java HTML Parser (org.jsoup:jsoup:1.22.1 - https://jsoup.org/) * mockito-core (org.mockito:mockito-core:3.12.4 - https://github.com/mockito/mockito) * mockito-inline (org.mockito:mockito-inline:3.12.4 - https://github.com/mockito/mockito) * Duct Tape (org.rnorth.duct-tape:duct-tape:1.0.8 - https://github.com/rnorth/duct-tape) * SLF4J API Module (org.slf4j:slf4j-api:2.0.17 - http://www.slf4j.org) - * Testcontainers Core (org.testcontainers:testcontainers:1.21.3 - https://java.testcontainers.org) + * Testcontainers Core (org.testcontainers:testcontainers:2.0.5 - https://java.testcontainers.org) * HAL Browser (org.webjars:hal-browser:ad9b865 - http://webjars.org) * toastr (org.webjars.bowergithub.codeseven:toastr:2.1.4 - http://webjars.org) * backbone (org.webjars.bowergithub.jashkenas:backbone:1.4.1 - https://www.webjars.org) @@ -701,7 +705,7 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines * jquery (org.webjars.bowergithub.jquery:jquery-dist:3.7.1 - https://www.webjars.org) * urijs (org.webjars.bowergithub.medialize:uri.js:1.19.11 - https://www.webjars.org) * bootstrap (org.webjars.bowergithub.twbs:bootstrap:4.6.2 - https://www.webjars.org) - * core-js (org.webjars.npm:core-js:3.46.0 - https://www.webjars.org) + * core-js (org.webjars.npm:core-js:3.49.0 - https://www.webjars.org) * @json-editor/json-editor (org.webjars.npm:json-editor__json-editor:2.15.2 - https://www.webjars.org) MIT-0: @@ -729,11 +733,11 @@ https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines The Apache Software License, version 2.0: - * picocli (info.picocli:picocli:4.7.6 - https://picocli.info) + * picocli (info.picocli:picocli:4.7.7 - https://picocli.info) UnRar License: - * Java Unrar (com.github.junrar:junrar:7.5.5 - https://github.com/junrar/junrar) + * Java Unrar (com.github.junrar:junrar:7.5.8 - https://github.com/junrar/junrar) Unicode/ICU License: From 544632c10883df7d149e68c8accb68c135c083e1 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Thu, 28 May 2026 10:28:46 -0500 Subject: [PATCH 695/701] [maven-release-plugin] prepare release dspace-8.4 --- dspace-api/pom.xml | 2 +- dspace-iiif/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-rdf/pom.xml | 2 +- dspace-server-webapp/pom.xml | 2 +- dspace-services/pom.xml | 2 +- dspace-sword/pom.xml | 2 +- dspace-swordv2/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- dspace/modules/pom.xml | 2 +- dspace/modules/server-boot/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- dspace/pom.xml | 2 +- pom.xml | 28 ++++++++++++++-------------- 14 files changed, 27 insertions(+), 27 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 167d35494205..261926c0e76c 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 56a43c395df4..15f509f778ba 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 7713769d2461..262249bc145c 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index 5d4b6c119c31..db24b3995688 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 64c2884c5f7f..3f226327fb94 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -14,7 +14,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 9557a6206a05..cc0af05c37a5 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 22dc03788a6e..ad02d31f1325 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index f5921baa4a88..27c4bee8e77c 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index b1da55b508c7..f094b1dfcaf1 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index f44aa1925006..b41455e53e0d 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 ../../pom.xml diff --git a/dspace/modules/server-boot/pom.xml b/dspace/modules/server-boot/pom.xml index e95360e59827..5e4b7fe9611c 100644 --- a/dspace/modules/server-boot/pom.xml +++ b/dspace/modules/server-boot/pom.xml @@ -11,7 +11,7 @@ modules org.dspace - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index db11e7f46f57..6523392f76fe 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -7,7 +7,7 @@ modules org.dspace - 8.4-SNAPSHOT + 8.4 .. diff --git a/dspace/pom.xml b/dspace/pom.xml index dc4b27aa65ff..7492408e81a4 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - 8.4-SNAPSHOT + 8.4 ../pom.xml diff --git a/pom.xml b/pom.xml index 827ac0d1ee89..aaf17ba9b1e2 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - 8.4-SNAPSHOT + 8.4 DSpace Parent Project DSpace open source software is a turnkey institutional repository application. @@ -1026,68 +1026,68 @@ org.dspace dspace-api - 8.4-SNAPSHOT + 8.4 org.dspace dspace-api test-jar - 8.4-SNAPSHOT + 8.4 test org.dspace.modules additions - 8.4-SNAPSHOT + 8.4 org.dspace.modules server classes - 8.4-SNAPSHOT + 8.4 org.dspace dspace-sword - 8.4-SNAPSHOT + 8.4 org.dspace dspace-swordv2 - 8.4-SNAPSHOT + 8.4 org.dspace dspace-oai - 8.4-SNAPSHOT + 8.4 org.dspace dspace-services - 8.4-SNAPSHOT + 8.4 org.dspace dspace-server-webapp test-jar - 8.4-SNAPSHOT + 8.4 test org.dspace dspace-rdf - 8.4-SNAPSHOT + 8.4 org.dspace dspace-iiif - 8.4-SNAPSHOT + 8.4 org.dspace dspace-server-webapp - 8.4-SNAPSHOT + 8.4 @@ -1943,7 +1943,7 @@ scm:git:git@github.com:DSpace/DSpace.git scm:git:git@github.com:DSpace/DSpace.git https://github.com/DSpace/DSpace - dspace-8_x + dspace-8.4 From dd5c593c58d68e0373ff4409d80693109d80e256 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Thu, 4 Jun 2026 14:56:00 -0400 Subject: [PATCH 696/701] LIBDRUM-1026. DSpace 8.4 updated to copied/derived files Updated files copied or derived from stock DSpace files to reflect any changes in their stock DSpace 8.4 counterparts. https://umd-dit.atlassian.net/browse/LIBDRUM-1026 --- Dockerfile.ant | 15 ++++------ Dockerfile.dev | 16 ++++++---- Dockerfile.dev-additions | 15 +++++----- .../config/spring/api/bitstore.xml | 29 +++++++++++++++++++ .../config/spring/api/core-services.xml | 5 ++-- .../config/spring/api/discovery.xml | 28 +++++++++--------- dspace/config/crosswalks/DIM2UmdDataCite.xsl | 26 ++++++++++++++++- .../src/main/resources/Messages.properties | 10 +++---- .../config/spring/api/workflow-actions.xml | 17 ++++------- .../config/spring/api/workflow.xml | 2 +- 10 files changed, 104 insertions(+), 59 deletions(-) diff --git a/Dockerfile.ant b/Dockerfile.ant index 4bcec223f186..8d5759dc6d8e 100644 --- a/Dockerfile.ant +++ b/Dockerfile.ant @@ -4,13 +4,8 @@ ARG JDK_VERSION=17 FROM docker.io/eclipse-temurin:${JDK_VERSION} AS ant_build -# Create the initial install deployment using ANT -ENV ANT_VERSION=1.10.13 -ENV ANT_HOME=/tmp/ant-$ANT_VERSION -ENV PATH=$ANT_HOME/bin:$PATH -# Download and install 'ant' -RUN mkdir $ANT_HOME && \ - curl --silent --show-error --location --fail --retry 5 --output /tmp/apache-ant.tar.gz \ - https://archive.apache.org/dist/ant/binaries/apache-ant-${ANT_VERSION}-bin.tar.gz && \ - tar -zx --strip-components=1 -f /tmp/apache-ant.tar.gz -C $ANT_HOME && \ - rm /tmp/apache-ant.tar.gz +# Install Apache Ant +RUN apt-get update \ + && apt-get install -y --no-install-recommends ant \ + && apt-get purge -y --auto-remove \ + && rm -rf /var/lib/apt/lists/* diff --git a/Dockerfile.dev b/Dockerfile.dev index 90e1e579a18c..5e3ceb33d43b 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -56,11 +56,13 @@ RUN ant init_installation update_configs update_code update_webapps # Step 3 - Start up DSpace via Runnable JAR FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL -WORKDIR $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Need host command for "[dspace]/bin/make-handle-config" RUN apt-get update \ && apt-get install -y --no-install-recommends host \ @@ -89,6 +91,8 @@ RUN apt-get update && \ jq && \ mkfifo /var/spool/postfix/public/pickup && \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +COPY dspace/src/main/docker/cron/postfix.sh /usr/local/bin/postfix.sh # End UMD Customization -# On startup, run DSpace Runnable JAR -ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"] +# On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) +ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"] diff --git a/Dockerfile.dev-additions b/Dockerfile.dev-additions index 64bcd62b688c..8480f203bf39 100644 --- a/Dockerfile.dev-additions +++ b/Dockerfile.dev-additions @@ -58,11 +58,13 @@ RUN ant init_installation update_configs update_code update_webapps # Step 3 - Start up DSpace via Runnable JAR FROM docker.io/eclipse-temurin:${JDK_VERSION} -# NOTE: DSPACE_INSTALL must align with the "dspace.dir" default configuration. -ENV DSPACE_INSTALL=/dspace +# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. +# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml +# "dspace__P__dir" is setting the value of the "dspace.dir" configuration. This is our installation directory. +ENV dspace__P__dir=/dspace # Copy the /dspace directory from 'ant_build' container to /dspace in this container -COPY --from=ant_build /dspace $DSPACE_INSTALL -WORKDIR $DSPACE_INSTALL +COPY --from=ant_build /dspace $dspace__P__dir +WORKDIR $dspace__P__dir # Need host command for "[dspace]/bin/make-handle-config" # UMD Customization # Commenting out, because no need for "make-handle-config" in dev environment @@ -89,6 +91,5 @@ RUN apt-get update && \ RUN mkdir -p $DSPACE_INSTALL/proquest/incoming $DSPACE_INSTALL/proquest/processed \ $DSPACE_INSTALL/proquest/csv $DSPACE_INSTALL/proquest/marc # End UMD Customization - -# On startup, run DSpace Runnable JAR -ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar", "--dspace.dir=$DSPACE_INSTALL"] +# On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) +ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"] diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/bitstore.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/bitstore.xml index 15bb3ef1580b..84fdc8282204 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/bitstore.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/bitstore.xml @@ -27,6 +27,9 @@ + + + @@ -34,6 +37,32 @@ + + + + + + + + + + + + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/core-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/core-services.xml index c3825da174be..f951d3e5fb7b 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/core-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/core-services.xml @@ -32,7 +32,7 @@ - + @@ -163,10 +163,11 @@ - + + diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml index 92917818132e..783e1e19ff5f 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml @@ -30,8 +30,6 @@ - - @@ -2250,7 +2248,7 @@ - + @@ -2261,7 +2259,7 @@ - + @@ -2275,7 +2273,7 @@ - + @@ -2287,7 +2285,7 @@ - + @@ -2299,7 +2297,7 @@ - + @@ -2311,7 +2309,7 @@ - + @@ -2323,7 +2321,7 @@ - + @@ -2334,7 +2332,7 @@ - + @@ -2346,7 +2344,7 @@ - + @@ -2358,7 +2356,7 @@ - + @@ -2370,7 +2368,7 @@ - + @@ -2382,7 +2380,7 @@ - + @@ -3265,7 +3263,7 @@ - + diff --git a/dspace/config/crosswalks/DIM2UmdDataCite.xsl b/dspace/config/crosswalks/DIM2UmdDataCite.xsl index 37153bd3a079..bf4746b67f34 100644 --- a/dspace/config/crosswalks/DIM2UmdDataCite.xsl +++ b/dspace/config/crosswalks/DIM2UmdDataCite.xsl @@ -344,7 +344,8 @@ company as well. We have to ensure to use URIs of our prefix as primary identifiers only. --> - + + @@ -352,15 +353,23 @@ + + + + + + + + @@ -649,4 +658,19 @@ + + + + + + https://orcid.org/ + ORCID + + + + + diff --git a/dspace/modules/additions/src/main/resources/Messages.properties b/dspace/modules/additions/src/main/resources/Messages.properties index f444c3a29872..0f3ee9a90743 100644 --- a/dspace/modules/additions/src/main/resources/Messages.properties +++ b/dspace/modules/additions/src/main/resources/Messages.properties @@ -72,20 +72,20 @@ org.dspace.checker.ResultsLogger.store-number org.dspace.checker.ResultsLogger.to-be-processed = To be processed org.dspace.checker.ResultsLogger.user-format-description = User format description org.dspace.checker.SimpleReporterImpl.bitstream-id = Bitstream Id -org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report for -org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report for +org.dspace.checker.SimpleReporterImpl.bitstream-not-found-report = The following is a BITSTREAM NOT FOUND report from +org.dspace.checker.SimpleReporterImpl.bitstream-will-no-longer-be-processed = The following is a BITSTREAM WILL NO LONGER BE PROCESSED report from org.dspace.checker.SimpleReporterImpl.check-id = Check Id org.dspace.checker.SimpleReporterImpl.checksum = Checksum org.dspace.checker.SimpleReporterImpl.checksum-algorithm = Checksum Algorithm org.dspace.checker.SimpleReporterImpl.checksum-calculated = Checksum Calculated -org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report for +org.dspace.checker.SimpleReporterImpl.checksum-did-not-match = The following is a CHECKSUM DID NOT MATCH report from org.dspace.checker.SimpleReporterImpl.checksum-expected = Checksum Expected org.dspace.checker.SimpleReporterImpl.date-range-to = to org.dspace.checker.SimpleReporterImpl.deleted = Deleted -org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report for +org.dspace.checker.SimpleReporterImpl.deleted-bitstream-intro = The following is a BITSTREAM SET DELETED report from org.dspace.checker.SimpleReporterImpl.description = Description org.dspace.checker.SimpleReporterImpl.format-id = Format Id -org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker with the -u option +org.dspace.checker.SimpleReporterImpl.howto-add-unchecked-bitstreams = To add these bitstreams to be checked run the checksum checker again org.dspace.checker.SimpleReporterImpl.internal-id = Internal Id org.dspace.checker.SimpleReporterImpl.name = Name org.dspace.checker.SimpleReporterImpl.no-bitstreams-changed = There were no bitstreams found with changed checksums diff --git a/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml b/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml index f0089dd9e474..24667d12b402 100644 --- a/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml +++ b/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml @@ -2,7 +2,9 @@ + xmlns:util="http://www.springframework.org/schema/util" + xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd + http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd"> @@ -21,7 +23,6 @@ - @@ -44,7 +45,6 @@ - @@ -64,21 +64,14 @@ - - - - - - - + - - + diff --git a/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow.xml b/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow.xml index 17982626ad2d..29cd2472ff98 100644 --- a/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow.xml +++ b/dspace/modules/additions/src/test/data/dspaceFolder/config/spring/api/workflow.xml @@ -286,4 +286,4 @@ - \ No newline at end of file + From b4809bea3b3a3722dd1a7d994cc78ed6ae630b89 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Fri, 5 Jun 2026 08:34:12 -0400 Subject: [PATCH 697/701] LIBDRUM-1026. Updated timezone in EtdLoaderTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated the timezone from "IST" to "UTC" in EtdLoaderTest, in order for the test to pass. This change is likely necessary because of the addition of the “setDefaultTimeZone” method in dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java, which defaults the timezone to UTC. See https://github.com/dspace/DSpace/commit/c8ee72c0f27e5b8fa6e821f577ca574e7897ed3f https://umd-dit.atlassian.net/browse/LIBDRUM-1026 --- .../epo/service/EpoImportMetadataSourceServiceImpl.java | 1 - .../src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java | 2 +- .../rest/repository/CommunityCommunityGroupLinkRepository.java | 2 +- .../rest/repository/CommunityGroupCommunityLinkRepository.java | 2 +- .../dspace/app/rest/repository/EPersonLdapLinkRepository.java | 2 +- .../app/rest/repository/EtdUnitCollectionLinkRepository.java | 2 +- .../org/dspace/app/rest/repository/UnitGroupLinkRepository.java | 2 +- 7 files changed, 6 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index f3711fbb5f78..60ed389383da 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -30,7 +30,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpException; import org.apache.http.client.utils.URIBuilder; -import org.apache.jena.ext.xerces.impl.dv.util.Base64; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.util.XMLUtils; diff --git a/dspace/modules/additions/src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java b/dspace/modules/additions/src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java index 847b5cb4ffee..194662eafb31 100644 --- a/dspace/modules/additions/src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java +++ b/dspace/modules/additions/src/test/java/edu/umd/lib/dspace/app/EtdLoaderTest.java @@ -117,7 +117,7 @@ public void testMainEmbargoedItem() throws Exception { String logOutput = etdLogger.getLog(); assertThat(logOutput, containsString("Records written: 1")); assertThat(logOutput, containsString("Embargoes: 1")); - assertThat(logOutput, containsString("Embargoed until Tue Jun 26 00:00:00 IST 3027")); + assertThat(logOutput, containsString("Embargoed until Tue Jun 26 00:00:00 UTC 3027")); } } diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityCommunityGroupLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityCommunityGroupLinkRepository.java index 6e6dc62cbda5..36307babd739 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityCommunityGroupLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityCommunityGroupLinkRepository.java @@ -9,8 +9,8 @@ import java.sql.SQLException; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.CommunityGroupRest; import org.dspace.app.rest.model.CommunityRest; diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityGroupCommunityLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityGroupCommunityLinkRepository.java index eb8f7dbcd30f..8b41d0caeea9 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityGroupCommunityLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/CommunityGroupCommunityLinkRepository.java @@ -9,8 +9,8 @@ import java.util.LinkedList; import java.util.List; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.CommunityGroupRest; diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EPersonLdapLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EPersonLdapLinkRepository.java index 760df2e31d11..92edd7cb5bfa 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EPersonLdapLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EPersonLdapLinkRepository.java @@ -4,12 +4,12 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; -import javax.annotation.Nullable; import javax.naming.NamingException; import edu.umd.lib.dspace.authenticate.LdapService; import edu.umd.lib.dspace.authenticate.impl.Ldap; import edu.umd.lib.dspace.authenticate.impl.LdapServiceImpl; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.EPersonRest; import org.dspace.app.rest.model.GroupRest; diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EtdUnitCollectionLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EtdUnitCollectionLinkRepository.java index a9ea3a815d24..6bdd2184c912 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EtdUnitCollectionLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/EtdUnitCollectionLinkRepository.java @@ -2,8 +2,8 @@ import java.sql.SQLException; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.EtdUnitRest; diff --git a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/UnitGroupLinkRepository.java b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/UnitGroupLinkRepository.java index b6e422672763..736696e9e27b 100644 --- a/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/UnitGroupLinkRepository.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/rest/repository/UnitGroupLinkRepository.java @@ -2,8 +2,8 @@ import java.sql.SQLException; import java.util.UUID; -import javax.annotation.Nullable; +import jakarta.annotation.Nullable; import jakarta.servlet.http.HttpServletRequest; import org.dspace.app.rest.model.GroupRest; import org.dspace.app.rest.model.UnitRest; From 2cba118f05e9b0270887cbeb8910cc2655cfd880 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Fri, 5 Jun 2026 09:11:04 -0400 Subject: [PATCH 698/701] LIBDRUM-1026. Fixed failing UmdExtendedJsonAccessLogValveTest Fixed test failure in UmdExtendedJsonAccessLogValveTest.java: ``` [ERROR] org.dspace.app.UmdExtendedJsonAccessLogValveTest.testCommonLogFileFormat -- Time elapsed: 0.013 s <<< ERROR! java.lang.NullPointerException: temporal at java.base/java.util.Objects.requireNonNull(Objects.java:235) at java.base/java.time.Instant.from(Instant.java:373) at org.apache.catalina.valves.AbstractAccessLogValve$DateAndTimeElement.addElement(AbstractAccessLogValve.java:1107) at org.apache.catalina.valves.JsonAccessLogValve$JsonWrappedElement.addElement(JsonAccessLogValve.java:264) at org.apache.catalina.valves.AbstractAccessLogValve.log(AbstractAccessLogValve.java:672) at org.dspace.app.UmdExtendedJsonAccessLogValveTest.simulateRequest(UmdExtendedJsonAccessLogValveTest.java:269) at org.dspace.app.UmdExtendedJsonAccessLogValveTest.simulateRequest(UmdExtendedJsonAccessLogValveTest.java:219) ... ``` by mocking the `getStartInstant()` method in the request object to return the epoch start. Also in the `log` method call, changed "bytes" the "requestDuration", set to an arbitrary value, because the method signature indicates that it is expecting the time spent on the request, not the number of bytes (it is unclear why this was initially thought to be bytes). https://umd-dit.atlassian.net/browse/LIBDRUM-1026 --- .../org/dspace/app/UmdExtendedJsonAccessLogValveTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dspace/modules/server-boot/src/test/java/org/dspace/app/UmdExtendedJsonAccessLogValveTest.java b/dspace/modules/server-boot/src/test/java/org/dspace/app/UmdExtendedJsonAccessLogValveTest.java index 563cff767385..fee328b0fb45 100644 --- a/dspace/modules/server-boot/src/test/java/org/dspace/app/UmdExtendedJsonAccessLogValveTest.java +++ b/dspace/modules/server-boot/src/test/java/org/dspace/app/UmdExtendedJsonAccessLogValveTest.java @@ -8,6 +8,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.Collections; import java.util.Date; import java.util.Enumeration; @@ -244,6 +245,7 @@ private void simulateRequest( when(mockRequest.getCoyoteRequest()).thenReturn(mockCoyoteRequest); // Arbitrarily set "time" at epoch start when(mockCoyoteRequest.getStartTime()).thenReturn(0l); + when(mockCoyoteRequest.getStartInstant()).thenReturn(Instant.ofEpochSecond(0l)); // Set up the mock Response to return expected values when(mockRequest.getRemoteHost()).thenReturn(remoteIP); @@ -266,7 +268,8 @@ private void simulateRequest( when(mockRequest.getHeaders("User-Agent")).thenReturn(enumUserAgent); // Invoke the logging logic of the JsonAccessLogValve - valve.log(mockRequest, mockResponse, bytes); + long requestDuration = 42l; // Number is arbitrary + valve.log(mockRequest, mockResponse, requestDuration); } /** From 2d3338122ba88dde96153afe46f7735828751d88 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Fri, 5 Jun 2026 11:00:52 -0400 Subject: [PATCH 699/701] LIBDRUM-1026. Ignoring S3BitStoreServiceIT integration test The org.dspace.storage.bitstore.S3BitStoreServiceIT test consistently fails in Jenkins with: ``` java.lang.IllegalStateException: Could not find a valid Docker environment. Please see logs and check configuration at org.testcontainers.dockerclient.DockerClientProviderStrategy.lambda$getFirstValidStrategy$7(DockerClientProviderStrategy.java:274) at java.base/java.util.Optional.orElseThrow(Optional.java:403) at org.testcontainers.dockerclient.DockerClientProviderStrategy.getFirstValidStrategy(DockerClientProviderStrategy.java:265) at org.testcontainers.DockerClientFactory.getOrInitializeStrategy(DockerClientFactory.java:154) at org.testcontainers.DockerClientFactory.client(DockerClientFactory.java:196) at org.testcontainers.DockerClientFactory$1.getDockerClient(DockerClientFactory.java:108) at com.github.dockerjava.api.DockerClientDelegate.authConfig(DockerClientDelegate.java:111) at org.testcontainers.containers.GenericContainer.start(GenericContainer.java:316) at org.dspace.storage.bitstore.S3BitStoreServiceIT.setupS3(S3BitStoreServiceIT.java:85) ... ``` See DSpace PR 11900 for discussion. While there is a fix in DSpace 8.4 (see DSpace PR 11901), this does not seem to work in Jenkins. From a comment in DSpace PR 11900: > It seems like this testcontainers dependency is just not fully > "stable" in all developer environments. I wish there was something > else we could switch to, but I don't have another option. Attempted a workaround by adding a `DOCKER_API_VERSION=1.44` into Jenkins, but it did not seem to work. https://umd-dit.atlassian.net/browse/LIBDRUM-1026 --- .../java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java b/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java index c1c1ed3ccc77..37c2b1e0dd2a 100644 --- a/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/storage/bitstore/S3BitStoreServiceIT.java @@ -55,6 +55,7 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; +import org.junit.Ignore; import org.junit.Test; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.awscore.exception.AwsServiceException; @@ -67,6 +68,9 @@ /** * @author Luca Giamminonni (luca.giamminonni at 4science.com) */ +// UMD Customization +@Ignore("UMD - These tests consistently fail when run on Jenkins, see https://github.com/DSpace/DSpace/pull/11900") +// End UMD Customization public class S3BitStoreServiceIT extends AbstractIntegrationTestWithDatabase { private static S3MockContainer s3Mock = new S3MockContainer("4.8.0"); From e0e98d1ee65d3dd18ef808a85bab542371b29ef0 Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Mon, 8 Jun 2026 13:58:04 -0400 Subject: [PATCH 700/701] LIBDRUM-1026. Removed "/var/spool/postfix/public/pickup" dir creation Removed creation of the "/var/spool/postfix/public/pickup" directory used by Postfix in sending emails, because: * It was causing the "Dockerfile.dev" Docker image build to fail * The directory should be automatically created by the `apt-get install` of postfix Also updated the instructions in "DockerDevelopmentEnvironment.md" that included the directory creation when adding email handling to the "Dockerfile.dev-additons" Docker image. Testing with Docker images created without the creation command confirmed the command was not needed. https://umd-dit.atlassian.net/browse/LIBDRUM-1026 --- Dockerfile.dev | 1 - dspace/docs/DockerDevelopmentEnvironment.md | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 5e3ceb33d43b..6e5e72e9c4f9 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -89,7 +89,6 @@ RUN apt-get update && \ vim \ python3-lxml \ jq && \ - mkfifo /var/spool/postfix/public/pickup && \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY dspace/src/main/docker/cron/postfix.sh /usr/local/bin/postfix.sh diff --git a/dspace/docs/DockerDevelopmentEnvironment.md b/dspace/docs/DockerDevelopmentEnvironment.md index 6a3a280c237c..f0285840e9e8 100644 --- a/dspace/docs/DockerDevelopmentEnvironment.md +++ b/dspace/docs/DockerDevelopmentEnvironment.md @@ -306,8 +306,7 @@ RUN apt-get update && \ libconfig-properties-perl \ jq \ && apt-get purge -y --auto-remove \ - && rm -rf /var/lib/apt/lists/* \ - && mkfifo /var/spool/postfix/public/pickup + && rm -rf /var/lib/apt/lists/* # End Dependencies for email functionality ``` From 43ceb8fff7513e56e8ab52226b261b8b409277af Mon Sep 17 00:00:00 2001 From: "David P. Steelman" Date: Tue, 9 Jun 2026 10:35:13 -0400 Subject: [PATCH 701/701] LIBDRUM-1026. Updated "Dockerfile.dev-additions" to use dspace__P__dir Updated "Dockerfile.dev-additions" to use the "dspace__P__dir" environment variable, instead of "DSPACE_INSTALL", following the example in "Dockerfile" and "Dockerfile.dev". https://umd-dit.atlassian.net/browse/LIBDRUM-1026 --- Dockerfile.dev-additions | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.dev-additions b/Dockerfile.dev-additions index 8480f203bf39..9d365e3d21e2 100644 --- a/Dockerfile.dev-additions +++ b/Dockerfile.dev-additions @@ -88,8 +88,8 @@ RUN apt-get update && \ jq # Create the directories needed for Proquest ETD loading -RUN mkdir -p $DSPACE_INSTALL/proquest/incoming $DSPACE_INSTALL/proquest/processed \ - $DSPACE_INSTALL/proquest/csv $DSPACE_INSTALL/proquest/marc +RUN mkdir -p $dspace__P__dir/proquest/incoming $dspace__P__dir/proquest/processed \ + $dspace__P__dir/proquest/csv $dspace__P__dir/proquest/marc # End UMD Customization # On startup, run DSpace Runnable JAR (uses the "dspace.dir" setting defined in "dspace__P__dir" env variable) ENTRYPOINT ["java", "-jar", "webapps/server-boot.jar"]