From 51d8874a8fdf9488615aab7d80e7597f5b0d2341 Mon Sep 17 00:00:00 2001 From: jensroets Date: Thu, 8 Sep 2022 16:50:02 +0200 Subject: [PATCH 01/63] 94299 Multiple Bitstream deletion endpoint --- .../app/rest/RestResourceController.java | 33 + .../repository/BitstreamRestRepository.java | 44 + .../rest/repository/DSpaceRestRepository.java | 18 + .../app/rest/BitstreamRestRepositoryIT.java | 955 ++++++++++++++++++ 4 files changed, 1050 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java index b82b483075..24468660f0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest; +import static org.dspace.app.rest.utils.ContextUtil.obtainContext; import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_DIGIT; import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_HEX32; import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG; @@ -55,6 +56,8 @@ import org.dspace.app.rest.repository.LinkRestRepository; import org.dspace.app.rest.utils.RestRepositoryUtils; import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.core.Context; import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; @@ -1050,6 +1053,13 @@ public class RestResourceController implements InitializingBean { return deleteInternal(apiCategory, model, uuid); } + @RequestMapping(method = RequestMethod.DELETE, consumes = {"text/uri-list"}) + public ResponseEntity> delete(HttpServletRequest request, @PathVariable String apiCategory, + @PathVariable String model) + throws HttpRequestMethodNotSupportedException { + return deleteUriListInternal(request, apiCategory, model); + } + /** * Internal method to delete resource. * @@ -1067,6 +1077,29 @@ public class RestResourceController implements InitializingBean { return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } + public ResponseEntity> deleteUriListInternal( + HttpServletRequest request, + String apiCategory, + String model) + throws HttpRequestMethodNotSupportedException { + checkModelPluralForm(apiCategory, model); + DSpaceRestRepository repository = utils.getResourceRepository(apiCategory, model); + Context context = obtainContext(request); + List dsoStringList = utils.getStringListFromRequest(request); + List dsoList = utils.constructDSpaceObjectList(context, dsoStringList); + if (dsoStringList.size() != dsoList.size()) { + throw new ResourceNotFoundException("One or more bitstreams could not be found."); + } + try { + repository.delete(dsoList); + } catch (ClassCastException e) { + log.error("Something went wrong whilst creating the object for apiCategory: " + apiCategory + + " and model: " + model, e); + return ControllerUtils.toEmptyResponse(HttpStatus.INTERNAL_SERVER_ERROR); + } + return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); + } + /** * Execute a PUT request for an entity with id of type UUID; * diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java index ae3cf91d4c..f599d993be 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java @@ -10,6 +10,8 @@ package org.dspace.app.rest.repository; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -147,6 +149,48 @@ public class BitstreamRestRepository extends DSpaceObjectRestRepository dsoList) + throws SQLException, AuthorizeException { + // check if list is empty + if (dsoList.isEmpty()) { + throw new ResourceNotFoundException("No bitstreams given."); + } + // check if every DSO is a Bitstream + if (dsoList.stream().anyMatch(dso -> !(dso instanceof Bitstream))) { + throw new UnprocessableEntityException("Not all given items are bitstreams."); + } + // check that they're all part of the same Item + List items = new ArrayList<>(); + for (DSpaceObject dso : dsoList) { + Bitstream bit = bs.find(context, dso.getID()); + DSpaceObject bitstreamParent = bs.getParentObject(context, bit); + if (bit == null) { + throw new ResourceNotFoundException("The bitstream with uuid " + dso.getID() + " could not be found"); + } + // we have to check if the bitstream has already been deleted + if (bit.isDeleted()) { + throw new UnprocessableEntityException("The bitstream with uuid " + bit.getID() + + " was already deleted"); + } else { + items.add(bitstreamParent); + } + } + if (items.stream().distinct().count() > 1) { + throw new UnprocessableEntityException("Not all given items are part of the same Item."); + } + // delete all Bitstreams + Iterator iterator = dsoList.iterator(); + while (iterator.hasNext()) { + Bitstream bit = (Bitstream) iterator.next(); + try { + bs.delete(context, bit); + } catch (SQLException | IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + } + /** * Find the bitstream for the provided handle and sequence or filename. * When a bitstream can be found with the sequence ID it will be returned if the user has "METADATA_READ" access. diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java index 01f127eca5..219b7c4123 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java @@ -26,6 +26,7 @@ import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.patch.Patch; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; import org.dspace.content.service.MetadataFieldService; import org.dspace.core.Context; import org.springframework.beans.factory.BeanNameAware; @@ -256,6 +257,23 @@ public abstract class DSpaceRestRepository dsoList) { + Context context = obtainContext(); + try { + getThisRepository().deleteList(context, dsoList); + context.commit(); + } catch (AuthorizeException e) { + throw new RESTAuthorizationException(e); + } catch (SQLException ex) { + throw new RuntimeException(ex.getMessage(), ex); + } + } + + protected void deleteList(Context context, List list) + throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException { + throw new RepositoryMethodNotImplementedException("No implementation found; Method not allowed!", ""); + } + @Override /** * This method cannot be implemented we required all the find method to be paginated diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index f9c1e469fc..391d9e4193 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -13,6 +13,7 @@ import static org.dspace.core.Constants.WRITE; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST; 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.result.MockMvcResultMatchers.content; @@ -1201,6 +1202,960 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest .andExpect(status().isNotFound()); } + @Test + public void deleteListOneBitstream() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + String bitstreamContent = "ThisIsSomeDummyText"; + //Add a bitstream to an item + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream") + .withDescription("Description") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream.getID())) + .andExpect(status().is(204)); + + // Verify 404 after delete + getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteListOneOfMultipleBitstreams() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 3 bitstreams to the item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent3 = "ThisIsSomeDummyText3"; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { + bitstream3 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream3") + .withDescription("Description3") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete bitstream1 + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().is(204)); + + // Verify 404 after delete for bitstream1 + getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isNotFound()); + + // check that bitstream2 still exists + getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", HalMatcher.matchNoEmbeds())); + + // check that bitstream3 still exists + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", HalMatcher.matchNoEmbeds())) + ; + } + + @Test + public void deleteListAllBitstreams() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 3 bitstreams to the item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent3 = "ThisIsSomeDummyText3"; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { + bitstream3 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream3") + .withDescription("Description3") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete all bitstreams + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().is(204)); + + // Verify 404 after delete for bitstream1 + getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isNotFound()); + + // Verify 404 after delete for bitstream2 + getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isNotFound()); + + // Verify 404 after delete for bitstream3 + getClient(token).perform(get("/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteListForbidden() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 3 bitstreams to the item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent3 = "ThisIsSomeDummyText3"; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { + bitstream3 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream3") + .withDescription("Description3") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(eperson.getEmail(), password); + + // Delete using an unauthorized user + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isForbidden()); + + // Verify the bitstreams are still here + getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isOk()); + } + + @Test + public void deleteListUnauthorized() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 3 bitstreams to the item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent3 = "ThisIsSomeDummyText3"; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { + bitstream3 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream3") + .withDescription("Description3") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + // Delete as anonymous + getClient().perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isUnauthorized()); + + // Verify the bitstreams are still here + getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isOk()); + } + + @Test + public void deleteListEmpty() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 3 bitstreams to the item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent3 = "ThisIsSomeDummyText3"; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { + bitstream3 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream3") + .withDescription("Description3") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete with empty list throws 404 + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("")) + .andExpect(status().isNotFound()); + + // Verify the bitstreams are still here + getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isOk()); + + getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isOk()); + + getClient(token).perform(get("/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isOk()); + } + + @Test + public void deleteListNotBitstream() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 3 bitstreams to the item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent3 = "ThisIsSomeDummyText3"; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { + bitstream3 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream3") + .withDescription("Description3") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete with list containing non-Bitstream throws 422 + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID() + + " \n http://localhost:8080/server/api/core/items/" + publicItem1.getID())) + .andExpect(status().is(422)); + + // Verify the bitstreams are still here + getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isOk()); + + getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isOk()); + + getClient(token).perform(get("/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isOk()); + } + + @Test + public void deleteListDifferentItems() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. Two public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + Item publicItem2 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 1 bitstream to each item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem2, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete with list containing Bitstreams from different items throws 422 + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().is(422)); + + // Verify the bitstreams are still here + getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isOk()); + + getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isOk()); + + } + + @Test + public void deleteListLogo() throws Exception { + // We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + // ** GIVEN ** + // 1. A community with a logo + parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").withLogo("logo_community") + .build(); + + // 2. A collection with a logo + Collection col = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection") + .withLogo("logo_collection").build(); + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // trying to DELETE parentCommunity logo and collection logo should work + // we have to delete them separately otherwise it will throw 422 as they belong to different items + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + parentCommunity.getLogo().getID())) + .andExpect(status().is(204)); + + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + col.getLogo().getID())) + .andExpect(status().is(204)); + + // Verify 404 after delete for parentCommunity logo + getClient(token).perform(get("/api/core/bitstreams/" + parentCommunity.getLogo().getID())) + .andExpect(status().isNotFound()); + + // Verify 404 after delete for collection logo + getClient(token).perform(get("/api/core/bitstreams/" + col.getLogo().getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteListMissing() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + + // Delete + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb")) + .andExpect(status().isNotFound()); + + // Verify 404 after failed delete + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb")) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteListOneMissing() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 3 bitstreams to the item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent3 = "ThisIsSomeDummyText3"; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { + bitstream3 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream3") + .withDescription("Description3") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete all bitstreams and a missing bitstream returns 404 + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb")) + .andExpect(status().isNotFound()); + + // Verify the bitstreams are still here + getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isOk()); + + getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isOk()); + + getClient(token).perform(get("/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().isOk()); + } + + @Test + public void deleteListOneMissingDifferentItems() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. Two public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + Item publicItem2 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 1 bitstream to each item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem2, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete all bitstreams and a missing bitstream returns 404 + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb")) + .andExpect(status().isNotFound()); + + // Verify the bitstreams are still here + getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().isOk()); + + getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) + .andExpect(status().isOk()); + + } + + @Test + public void deleteListDeleted() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + String bitstreamContent = "ThisIsSomeDummyText"; + //Add a bitstream to an item + Bitstream bitstream = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream") + .withDescription("Description") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream.getID())) + .andExpect(status().is(204)); + + // Verify 404 when trying to delete a non-existing, already deleted, bitstream + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream.getID())) + .andExpect(status().is(422)); + } + + @Test + public void deleteListOneDeleted() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + 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(); + + //2. One public items that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .withSubject("ExtraEntry") + .build(); + + // Add 3 bitstreams to the item + String bitstreamContent1 = "ThisIsSomeDummyText1"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream1") + .withDescription("Description1") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "ThisIsSomeDummyText2"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream2") + .withDescription("Description2") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent3 = "ThisIsSomeDummyText3"; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { + bitstream3 = BitstreamBuilder. + createBitstream(context, publicItem1, is) + .withName("Bitstream3") + .withDescription("Description3") + .withMimeType("text/plain") + .build(); + } + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + // Delete bitstream1 + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID())) + .andExpect(status().is(204)); + + // Verify 404 when trying to delete a non-existing, already deleted, bitstream + getClient(token).perform(delete("/api/core/bitstreams") + .contentType(TEXT_URI_LIST) + .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() + + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID())) + .andExpect(status().is(422)); + } + @Test public void patchBitstreamMetadataAuthorized() throws Exception { runPatchMetadataTests(admin, 200); From 464465560187002f0d50dbd0f6a9f12044a42723 Mon Sep 17 00:00:00 2001 From: jensroets Date: Wed, 14 Sep 2022 15:49:03 +0200 Subject: [PATCH 02/63] 94299 Multiple Bitstream deletion endpoint: rename items to parents --- .../dspace/app/rest/repository/BitstreamRestRepository.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java index f599d993be..3696b38668 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java @@ -161,7 +161,7 @@ public class BitstreamRestRepository extends DSpaceObjectRestRepository items = new ArrayList<>(); + List parents = new ArrayList<>(); for (DSpaceObject dso : dsoList) { Bitstream bit = bs.find(context, dso.getID()); DSpaceObject bitstreamParent = bs.getParentObject(context, bit); @@ -173,10 +173,10 @@ public class BitstreamRestRepository extends DSpaceObjectRestRepository 1) { + if (parents.stream().distinct().count() > 1) { throw new UnprocessableEntityException("Not all given items are part of the same Item."); } // delete all Bitstreams From 8e2ada65b191d55bc86002bef10e2a4707cb4d2a Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 6 Dec 2022 12:36:34 +0100 Subject: [PATCH 03/63] 97248: Fix File info Solr plugin to allow faceting --- .../org/dspace/discovery/SolrServiceFileInfoPlugin.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 52e0043ff4..c53b48f80f 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -53,10 +53,14 @@ public class SolrServiceFileInfoPlugin implements SolrServiceIndexPlugin { if (bitstreams != null) { for (Bitstream bitstream : bitstreams) { document.addField(SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName()); + 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); + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword", bitstream.getName()); + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter", bitstream.getName()); } } } @@ -65,4 +69,4 @@ public class SolrServiceFileInfoPlugin implements SolrServiceIndexPlugin { } } } -} \ No newline at end of file +} From 3e651af7605853b013fe52607b0701f797090a28 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 6 Dec 2022 12:37:21 +0100 Subject: [PATCH 04/63] 97248: Find DSO based configurations recursively through parent objects --- .../org/dspace/discovery/SearchUtils.java | 45 ++++++++++++----- .../DiscoveryConfigurationService.java | 49 +++++++++++++++++-- .../CollectionIndexFactoryImpl.java | 4 +- .../CommunityIndexFactoryImpl.java | 4 +- .../InprogressSubmissionIndexFactoryImpl.java | 6 +-- .../indexobject/ItemIndexFactoryImpl.java | 2 +- .../repository/DiscoveryRestRepository.java | 10 ++-- 7 files changed, 89 insertions(+), 31 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java b/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java index 90afb09eca..83cbdeaef6 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java @@ -18,6 +18,7 @@ import org.dspace.content.Collection; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; +import org.dspace.core.Context; import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoveryConfigurationService; import org.dspace.kernel.ServiceManager; @@ -60,28 +61,32 @@ public class SearchUtils { } public static DiscoveryConfiguration getDiscoveryConfiguration() { - return getDiscoveryConfiguration(null, null); + return getDiscoveryConfiguration(null, null, null); } - public static DiscoveryConfiguration getDiscoveryConfiguration(DSpaceObject dso) { - return getDiscoveryConfiguration(null, dso); + public static DiscoveryConfiguration getDiscoveryConfiguration(final Context context, + DSpaceObject dso) { + return getDiscoveryConfiguration(context, null, dso); } /** * Return the discovery configuration to use in a specific scope for the king of search identified by the prefix. A * null prefix mean the normal query, other predefined values are workspace or workflow * + * + * @param context * @param prefix * the namespace of the configuration to lookup if any * @param dso * the DSpaceObject * @return the discovery configuration for the specified scope */ - public static DiscoveryConfiguration getDiscoveryConfiguration(String prefix, DSpaceObject dso) { + public static DiscoveryConfiguration getDiscoveryConfiguration(final Context context, String prefix, + DSpaceObject dso) { if (prefix != null) { return getDiscoveryConfigurationByName(dso != null ? prefix + "." + dso.getHandle() : prefix); } else { - return getDiscoveryConfigurationByName(dso != null ? dso.getHandle() : null); + return getDiscoveryConfigurationByDSO(context, dso); } } @@ -98,6 +103,11 @@ public class SearchUtils { return configurationService.getDiscoveryConfiguration(configurationName); } + public static DiscoveryConfiguration getDiscoveryConfigurationByDSO( + Context context, DSpaceObject dso) { + DiscoveryConfigurationService configurationService = getConfigurationService(); + return configurationService.getDiscoveryDSOConfiguration(context, dso); + } public static DiscoveryConfigurationService getConfigurationService() { ServiceManager manager = DSpaceServicesFactory.getInstance().getServiceManager(); @@ -114,45 +124,54 @@ public class SearchUtils { * A configuration object can be returned for each parent community/collection * * @param item the DSpace item + * @param context * @return a list of configuration objects * @throws SQLException An exception that provides information on a database access error or other errors. */ - public static List getAllDiscoveryConfigurations(Item item) throws SQLException { + public static List getAllDiscoveryConfigurations(Item item, + final Context context) throws SQLException { List collections = item.getCollections(); - return getAllDiscoveryConfigurations(null, collections, item); + return getAllDiscoveryConfigurations(context, null, collections, item); } /** * Return all the discovery configuration applicable to the provided workspace item + * + * @param context * @param witem a workspace item * @return a list of discovery configuration * @throws SQLException */ - public static List getAllDiscoveryConfigurations(WorkspaceItem witem) throws SQLException { + public static List getAllDiscoveryConfigurations(final Context context, + WorkspaceItem witem) throws SQLException { List collections = new ArrayList(); collections.add(witem.getCollection()); - return getAllDiscoveryConfigurations("workspace", collections, witem.getItem()); + return getAllDiscoveryConfigurations(context, "workspace", collections, witem.getItem()); } /** * Return all the discovery configuration applicable to the provided workflow item + * + * @param context * @param witem a workflow item * @return a list of discovery configuration * @throws SQLException */ - public static List getAllDiscoveryConfigurations(WorkflowItem witem) throws SQLException { + public static List getAllDiscoveryConfigurations(final Context context, + WorkflowItem witem) throws SQLException { List collections = new ArrayList(); collections.add(witem.getCollection()); - return getAllDiscoveryConfigurations("workflow", collections, witem.getItem()); + return getAllDiscoveryConfigurations(context, "workflow", collections, witem.getItem()); } - private static List getAllDiscoveryConfigurations(String prefix, + private static List getAllDiscoveryConfigurations(final Context context, + String prefix, List collections, Item item) throws SQLException { Set result = new HashSet<>(); for (Collection collection : collections) { - DiscoveryConfiguration configuration = getDiscoveryConfiguration(prefix, collection); + DiscoveryConfiguration configuration = getDiscoveryConfiguration(context, prefix, collection); result.add(configuration); } 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 636e7ccd2a..b00ff73563 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 @@ -7,12 +7,20 @@ */ package org.dspace.discovery.configuration; +import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.DSpaceObject; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.DSpaceObjectService; +import org.dspace.core.Context; +import org.dspace.core.ReloadableEntity; import org.dspace.discovery.IndexableObject; import org.dspace.discovery.indexobject.IndexableDSpaceObject; import org.dspace.services.factory.DSpaceServicesFactory; @@ -22,6 +30,8 @@ import org.dspace.services.factory.DSpaceServicesFactory; */ public class DiscoveryConfigurationService { + private static final Logger log = LogManager.getLogger(); + private Map map; private Map> toIgnoreMetadataFields = new HashMap<>(); @@ -41,25 +51,53 @@ public class DiscoveryConfigurationService { this.toIgnoreMetadataFields = toIgnoreMetadataFields; } - public DiscoveryConfiguration getDiscoveryConfiguration(IndexableObject dso) { + public DiscoveryConfiguration getDiscoveryConfiguration(final Context context, + IndexableObject dso) { String name; if (dso == null) { name = "default"; } else if (dso instanceof IndexableDSpaceObject) { - name = ((IndexableDSpaceObject) dso).getIndexedObject().getHandle(); + return getDiscoveryDSOConfiguration(context, ((IndexableDSpaceObject) dso).getIndexedObject()); } else { name = dso.getUniqueIndexID(); } - return getDiscoveryConfiguration(name); } + public DiscoveryConfiguration getDiscoveryDSOConfiguration(final Context context, + DSpaceObject dso) { + String name; + if (dso == null) { + name = "default"; + } else { + name = dso.getHandle(); + } + + DiscoveryConfiguration configuration = getDiscoveryConfiguration(name, false); + if (configuration != null) { + return configuration; + } + DSpaceObjectService dSpaceObjectService = + ContentServiceFactory.getInstance().getDSpaceObjectService(dso); + DSpaceObject parentObject = null; + try { + parentObject = dSpaceObjectService.getParentObject(context, dso); + } catch (SQLException e) { + log.error(e); + } + return getDiscoveryDSOConfiguration(context, parentObject); + } + public DiscoveryConfiguration getDiscoveryConfiguration(final String name) { + return getDiscoveryConfiguration(name, true); + } + + public DiscoveryConfiguration getDiscoveryConfiguration(final String name, boolean useDefault) { DiscoveryConfiguration result; result = StringUtils.isBlank(name) ? null : getMap().get(name); - if (result == null) { + if (result == null && useDefault) { //No specific configuration, get the default one result = getMap().get("default"); } @@ -68,11 +106,12 @@ public class DiscoveryConfigurationService { } public DiscoveryConfiguration getDiscoveryConfigurationByNameOrDso(final String configurationName, + final Context context, final IndexableObject dso) { if (StringUtils.isNotBlank(configurationName) && getMap().containsKey(configurationName)) { return getMap().get(configurationName); } else { - return getDiscoveryConfiguration(dso); + return getDiscoveryConfiguration(context, dso); } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/CollectionIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/CollectionIndexFactoryImpl.java index c2bacfe502..817be7848d 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/CollectionIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/CollectionIndexFactoryImpl.java @@ -86,7 +86,7 @@ public class CollectionIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl highlightedMetadataFields = new ArrayList<>(); @@ -173,4 +173,4 @@ public class CollectionIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl highlightedMetadataFields = new ArrayList<>(); @@ -135,4 +135,4 @@ public class CommunityIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl discoveryConfigurations; if (inProgressSubmission instanceof WorkflowItem) { - discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations((WorkflowItem) inProgressSubmission); + discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, (WorkflowItem) inProgressSubmission); } else if (inProgressSubmission instanceof WorkspaceItem) { - discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations((WorkspaceItem) inProgressSubmission); + discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, (WorkspaceItem) inProgressSubmission); } else { - discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(item); + discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(item, context); } indexableItemService.addDiscoveryFields(doc, context, item, discoveryConfigurations); indexableCollectionService.storeCommunityCollectionLocations(doc, locations); diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java index e9f18ae949..b417237f76 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java @@ -147,7 +147,7 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(item); + List discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(item, context); addDiscoveryFields(doc, context, indexableItem.getIndexedObject(), discoveryConfigurations); //mandatory facet to show status on mydspace diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java index 52224ef579..1962d44162 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java @@ -84,7 +84,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, scopeObject); + .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); return discoverConfigurationConverter.convert(discoveryConfiguration, utils.obtainProjection()); } @@ -96,7 +96,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { Context context = obtainContext(); IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, scopeObject); + .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); DiscoverResult searchResult = null; DiscoverQuery discoverQuery = null; @@ -121,7 +121,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, scopeObject); + .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); return discoverFacetConfigurationConverter.convert(configuration, dsoScope, discoveryConfiguration); } @@ -138,7 +138,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, scopeObject); + .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); DiscoverQuery discoverQuery = queryBuilder.buildFacetQuery(context, scopeObject, discoveryConfiguration, prefix, query, searchFilters, dsoTypes, page, facetName); @@ -157,7 +157,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { Pageable page = PageRequest.of(1, 1); IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, scopeObject); + .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); DiscoverResult searchResult = null; DiscoverQuery discoverQuery = null; From 82bc777e45dce2525e2754fc338d27e7630bad1d Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 13 Dec 2022 12:32:15 +0100 Subject: [PATCH 05/63] Fix issue with indexing and add tests --- .../org/dspace/discovery/SearchUtils.java | 32 +- .../discovery/SolrServiceFileInfoPlugin.java | 6 +- .../DiscoveryConfigurationService.java | 9 +- .../InprogressSubmissionIndexFactoryImpl.java | 6 +- .../org/dspace/builder/CommunityBuilder.java | 24 +- .../config/spring/api/discovery.xml | 3198 +++++++++++++++++ .../DiscoveryScopeBasedRestControllerIT.java | 595 +++ .../app/rest/matcher/FacetEntryMatcher.java | 11 + .../app/rest/matcher/FacetValueMatcher.java | 10 + machine.cfg | 19 + 10 files changed, 3889 insertions(+), 21 deletions(-) create mode 100644 dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java create mode 100644 machine.cfg diff --git a/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java b/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java index 83cbdeaef6..4085e1bbdf 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java @@ -18,6 +18,8 @@ import org.dspace.content.Collection; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoveryConfigurationService; @@ -72,7 +74,7 @@ public class SearchUtils { /** * Return the discovery configuration to use in a specific scope for the king of search identified by the prefix. A * null prefix mean the normal query, other predefined values are workspace or workflow - * + * * * @param context * @param prefix @@ -90,9 +92,28 @@ public class SearchUtils { } } + public static Set addDiscoveryConfigurationForParents( + Context context, Set configurations, String prefix, DSpaceObject dso) + throws SQLException { + if (dso == null) { + configurations.add(getDiscoveryConfigurationByName(null)); + return configurations; + } + if (prefix != null) { + configurations.add(getDiscoveryConfigurationByName(prefix + "." + dso.getHandle())); + } else { + configurations.add(getDiscoveryConfigurationByName(dso.getHandle())); + } + + DSpaceObjectService dSpaceObjectService = ContentServiceFactory.getInstance() + .getDSpaceObjectService(dso); + DSpaceObject parentObject = dSpaceObjectService.getParentObject(context, dso); + return addDiscoveryConfigurationForParents(context, configurations, prefix, parentObject); + } + /** * Return the discovery configuration identified by the specified name - * + * * @param configurationName the configuration name assigned to the bean in the * discovery.xml * @return the discovery configuration @@ -128,8 +149,8 @@ public class SearchUtils { * @return a list of configuration objects * @throws SQLException An exception that provides information on a database access error or other errors. */ - public static List getAllDiscoveryConfigurations(Item item, - final Context context) throws SQLException { + public static List getAllDiscoveryConfigurations(Item item, Context context) + throws SQLException { List collections = item.getCollections(); return getAllDiscoveryConfigurations(context, null, collections, item); } @@ -171,8 +192,7 @@ public class SearchUtils { Set result = new HashSet<>(); for (Collection collection : collections) { - DiscoveryConfiguration configuration = getDiscoveryConfiguration(context, prefix, collection); - result.add(configuration); + addDiscoveryConfigurationForParents(context, result, prefix, collection); } //Add alwaysIndex configurations 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 c53b48f80f..6bda2fc52d 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -59,8 +59,10 @@ public class SolrServiceFileInfoPlugin implements SolrServiceIndexPlugin { String description = bitstream.getDescription(); if ((description != null) && !description.isEmpty()) { document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description); - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword", bitstream.getName()); - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter", bitstream.getName()); + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword", + bitstream.getName()); + document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter", + bitstream.getName()); } } } 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 b00ff73563..22443aec22 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 @@ -20,7 +20,6 @@ import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; -import org.dspace.core.ReloadableEntity; import org.dspace.discovery.IndexableObject; import org.dspace.discovery.indexobject.IndexableDSpaceObject; import org.dspace.services.factory.DSpaceServicesFactory; @@ -135,9 +134,9 @@ public class DiscoveryConfigurationService { System.out.println(DSpaceServicesFactory.getInstance().getServiceManager().getServicesNames().size()); DiscoveryConfigurationService mainService = DSpaceServicesFactory.getInstance().getServiceManager() .getServiceByName( - DiscoveryConfigurationService.class - .getName(), - DiscoveryConfigurationService.class); + DiscoveryConfigurationService.class + .getName(), + DiscoveryConfigurationService.class); for (String key : mainService.getMap().keySet()) { System.out.println(key); @@ -165,7 +164,7 @@ public class DiscoveryConfigurationService { System.out.println("Recent submissions configuration:"); DiscoveryRecentSubmissionsConfiguration recentSubmissionConfiguration = discoveryConfiguration - .getRecentSubmissionConfiguration(); + .getRecentSubmissionConfiguration(); System.out.println("\tMetadata sort field: " + recentSubmissionConfiguration.getMetadataSortField()); System.out.println("\tMax recent submissions: " + recentSubmissionConfiguration.getMax()); diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/InprogressSubmissionIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/InprogressSubmissionIndexFactoryImpl.java index c3629b6362..ebedfc34b7 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/InprogressSubmissionIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/InprogressSubmissionIndexFactoryImpl.java @@ -73,9 +73,11 @@ public abstract class InprogressSubmissionIndexFactoryImpl // Add item metadata List discoveryConfigurations; if (inProgressSubmission instanceof WorkflowItem) { - discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, (WorkflowItem) inProgressSubmission); + discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, + (WorkflowItem) inProgressSubmission); } else if (inProgressSubmission instanceof WorkspaceItem) { - discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, (WorkspaceItem) inProgressSubmission); + discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, + (WorkspaceItem) inProgressSubmission); } else { discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(item, context); } diff --git a/dspace-api/src/test/java/org/dspace/builder/CommunityBuilder.java b/dspace-api/src/test/java/org/dspace/builder/CommunityBuilder.java index 5ba36af8f4..1f0e8fbd66 100644 --- a/dspace-api/src/test/java/org/dspace/builder/CommunityBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/CommunityBuilder.java @@ -32,27 +32,38 @@ public class CommunityBuilder extends AbstractDSpaceObjectBuilder { private Community community; + protected CommunityBuilder(Context context) { super(context); } public static CommunityBuilder createCommunity(final Context context) { CommunityBuilder builder = new CommunityBuilder(context); - return builder.create(); + return builder.create(null); + } + public static CommunityBuilder createCommunity(final Context context, String handle) { + CommunityBuilder builder = new CommunityBuilder(context); + return builder.create(handle); } - private CommunityBuilder create() { - return createSubCommunity(context, null); + private CommunityBuilder create(String handle) { + return createSubCommunity(context, null, handle); } public static CommunityBuilder createSubCommunity(final Context context, final Community parent) { CommunityBuilder builder = new CommunityBuilder(context); - return builder.createSub(parent); + return builder.createSub(parent, null); } - private CommunityBuilder createSub(final Community parent) { + public static CommunityBuilder createSubCommunity(final Context context, final Community parent, + final String handle) { + CommunityBuilder builder = new CommunityBuilder(context); + return builder.createSub(parent, handle); + } + + private CommunityBuilder createSub(final Community parent, String handle) { try { - community = communityService.create(parent, context); + community = communityService.create(parent, context, handle); } catch (Exception e) { e.printStackTrace(); return null; @@ -102,6 +113,7 @@ public class CommunityBuilder extends AbstractDSpaceObjectBuilder { @Override public Community build() { try { + communityService.update(context, community); context.dispatchEvents(); 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 new file mode 100644 index 0000000000..6ffcbe661c --- /dev/null +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml @@ -0,0 +1,3198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.rights + + + + + + + + + + + + + + + dc.rights + + + + + + + + dc.description.provenance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item + + withdrawn:true OR discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:WorkspaceItem OR search.resourcetype:XmlWorkflowItem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:PoolTask OR search.resourcetype:ClaimedTask + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:XmlWorkflowItem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:Publication + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:Person + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:Project + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:OrgUnit + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:JournalIssue + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:JournalVolume + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:Journal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND (entityType_keyword:OrgUnit OR entityType_keyword:Person) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:OrgUnit AND dc.type:FundingOrganization + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + + + + + + + + + + + relation.isAuthorOfPublication + + + + + + + + + + + relation.isProjectOfPublication + + + + + + + + + + + + relation.isOrgUnitOfPublication + + + + + + + + + + + relation.isPublicationOfJournalIssue + + + + + + + + + + + relation.isJournalOfPublication + + + + + + + + + + + dc.contributor.author + dc.creator + + + + + + + + + + + + + + + dspace.entity.type + + + + + + + + + + + + + + dc.subject.* + + + + + + + + + + + + + + dc.date.issued + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.type + + + + + + + + + dc.identifier + + + + + + + + + placeholder.placeholder.placeholder + + + + + + + + + + placeholder.placeholder.placeholder + + + + + + + + + person.jobTitle + + + + + + + + + + + + + + + person.knowsLanguage + + + + + + + + + + + + + person.birthDate + + + + + + + + + + + + + + + + + person.familyName + + + + + + + + + + + person.givenName + + + + + + + + + + + relation.isOrgUnitOfPerson + + + + + + + + + + + relation.isProjectOfPerson + + + + + + + + + + + relation.isPublicationOfAuthor + + + + + + + + + + + + organization.address.addressCountry + + + + + + + + + + + + + + + organization.address.addressLocality + + + + + + + + + + + + + + + organization.foundingDate + + + + + + + + + + + + + + + + organization.legalName + + + + + + + + + + + relation.isPersonOfOrgUnit + + + + + + + + + + + relation.isProjectOfOrgUnit + + + + + + + + + + + relation.isPublicationOfOrgUnit + + + + + + + + + + + creativework.keywords + + + + + + + + + + + + + + + creativework.datePublished + + + + + + + + + + + + + + + + publicationissue.issueNumber + + + + + + + + + + + relation.isPublicationOfJournalIssue + + + + + + + + + + + publicationVolume.volumeNumber + + + + + + + + + + + relation.isIssueOfJournalVolume + + + + + + + + + + + relation.isJournalOfVolume + + + + + + + + + + + creativework.publisher + + + + + + + + + + + + + + + creativework.editor + + + + + + + + + + + + + + + relation.isVolumeOfJournal + + + + + + + + + + + + + + placeholder.placeholder.placeholder + + + + + + + + + + relation.isOrgUnitOfProject + + + + + + + + + + + + relation.isPersonOfProject + + + + + + + + + + + + relation.isPublicationOfProject + + + + + + + + + + + relation.isContributorOfPublication + + + + + + + + + + + relation.isPublicationOfContributor + + + + + + + + + + + relation.isFundingAgencyOfProject + + + + + + + + + + + relation.isProjectOfFundingAgency + + + + + + + + + + + dc.test.parentcommunity1field + + + + + + + + + + + + + + + dc.test.subcommunity11field + + + + + + + + + + + + + + + dc.test.collection111field + + + + + + + + + + + + + + + dc.test.collection121field + + + + + + + + + + + + + + + dc.test.subcommunity21field + + + + + + + + + + + + + + dc.test.collection211field + + + + + + + + + + + + + + dc.test.collection221field + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java new file mode 100644 index 0000000000..a0edf1a0c7 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java @@ -0,0 +1,595 @@ +/** + * 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; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.matcher.FacetEntryMatcher; +import org.dspace.app.rest.matcher.FacetValueMatcher; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.MetadataFieldBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.service.CollectionService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerIntegrationTest { + + @Autowired + CollectionService collectionService; + + private Community community1; + private Community subcommunity11; + private Community subcommunity12; + private Collection collection111; + private Collection collection112; + private Collection collection121; + private Collection collection122; + + private Community community2; + private Community subcommunity21; + private Community subcommunity22; + private Collection collection211; + private Collection collection212; + private Collection collection221; + private Collection collection222; + + @Before + public void setUp() throws Exception { + super.setUp(); + + context.turnOffAuthorisationSystem(); + + MetadataFieldBuilder.createMetadataField(context, "test", "parentcommunity1field", "").build(); + MetadataFieldBuilder.createMetadataField(context, "test", "subcommunity11field", "").build(); + MetadataFieldBuilder.createMetadataField(context, "test", "collection111field", "").build(); + MetadataFieldBuilder.createMetadataField(context, "test", "collection121field", "").build(); + MetadataFieldBuilder.createMetadataField(context, "test", "subcommunity21field", "").build(); + MetadataFieldBuilder.createMetadataField(context, "test", "collection211field", "").build(); + MetadataFieldBuilder.createMetadataField(context, "test", "collection221field", "").build(); + + community1 = CommunityBuilder.createCommunity(context, "123456789/discovery-parent-community-1") + .build(); + subcommunity11 = CommunityBuilder + .createSubCommunity(context, community1, "123456789/discovery-sub-community-1-1") + .build(); + subcommunity12 = CommunityBuilder + .createSubCommunity(context, community1, "123456789/discovery-sub-community-1-2") + .build(); + collection111 = CollectionBuilder + .createCollection(context, subcommunity11, "123456789/discovery-collection-1-1-1") + .build(); + collection112 = CollectionBuilder + .createCollection(context, subcommunity11, "123456789/discovery-collection-1-1-2") + .build(); + collection121 = CollectionBuilder + .createCollection(context, subcommunity12, "123456789/discovery-collection-1-2-1") + .build(); + + collection122 = CollectionBuilder + .createCollection(context, subcommunity12, "123456789/discovery-collection-1-2-2") + .build(); + + community2 = CommunityBuilder.createCommunity(context, "123456789/discovery-parent-community-2") + .build(); + + + subcommunity21 = CommunityBuilder + .createSubCommunity(context, community2, "123456789/discovery-sub-community-2-1") + .build(); + subcommunity22 = CommunityBuilder + .createSubCommunity(context, community2, "123456789/discovery-sub-community-2-2") + .build(); + collection211 = CollectionBuilder + .createCollection(context, subcommunity21, "123456789/discovery-collection-2-1-1") + .build(); + collection212 = CollectionBuilder + .createCollection(context, subcommunity21, "123456789/discovery-collection-2-1-2") + .build(); + collection221 = CollectionBuilder + .createCollection(context, subcommunity22, "123456789/discovery-collection-2-2-1") + .build(); + collection222 = CollectionBuilder + .createCollection(context, subcommunity22, "123456789/discovery-collection-2-2-2") + .build(); + + + Item item111 = ItemBuilder.createItem(context, collection111) + .withMetadata("dc", "contributor", "author", "author-item111") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-item111") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-item111") + .withMetadata("dc", "test", "collection111field", "collection111field-item111") + .withMetadata("dc", "test", "collection121field", "collection121field-item111") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-item111") + .withMetadata("dc", "test", "collection211field", "collection211field-item111") + .withMetadata("dc", "test", "collection221field", "collection221field-item111") + .build(); + + Item item112 = ItemBuilder.createItem(context, collection112) + .withMetadata("dc", "contributor", "author", "author-item112") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-item112") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-item112") + .withMetadata("dc", "test", "collection111field", "collection111field-item112") + .withMetadata("dc", "test", "collection121field", "collection121field-item112") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-item112") + .withMetadata("dc", "test", "collection211field", "collection211field-item112") + .withMetadata("dc", "test", "collection221field", "collection221field-item112") + .build(); + + Item item121 = ItemBuilder.createItem(context, collection121) + .withMetadata("dc", "contributor", "author", "author-item121") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-item121") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-item121") + .withMetadata("dc", "test", "collection111field", "collection111field-item121") + .withMetadata("dc", "test", "collection121field", "collection121field-item121") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-item121") + .withMetadata("dc", "test", "collection211field", "collection211field-item121") + .withMetadata("dc", "test", "collection221field", "collection221field-item121") + .build(); + + Item item122 = ItemBuilder.createItem(context, collection122) + .withMetadata("dc", "contributor", "author", "author-item122") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-item122") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-item122") + .withMetadata("dc", "test", "collection111field", "collection111field-item122") + .withMetadata("dc", "test", "collection121field", "collection121field-item122") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-item122") + .withMetadata("dc", "test", "collection211field", "collection211field-item122") + .withMetadata("dc", "test", "collection221field", "collection221field-item122") + .build(); + + Item item211 = ItemBuilder.createItem(context, collection211) + .withMetadata("dc", "contributor", "author", "author-item211") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-item211") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-item211") + .withMetadata("dc", "test", "collection111field", "collection111field-item211") + .withMetadata("dc", "test", "collection121field", "collection121field-item211") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-item211") + .withMetadata("dc", "test", "collection211field", "collection211field-item211") + .withMetadata("dc", "test", "collection221field", "collection221field-item211") + .build(); + + Item item212 = ItemBuilder.createItem(context, collection212) + .withMetadata("dc", "contributor", "author", "author-item212") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-item212") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-item212") + .withMetadata("dc", "test", "collection111field", "collection111field-item212") + .withMetadata("dc", "test", "collection121field", "collection121field-item212") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-item212") + .withMetadata("dc", "test", "collection211field", "collection211field-item212") + .withMetadata("dc", "test", "collection221field", "collection221field-item212") + .build(); + + Item item221 = ItemBuilder.createItem(context, collection221) + .withMetadata("dc", "contributor", "author", "author-item221") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-item221") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-item221") + .withMetadata("dc", "test", "collection111field", "collection111field-item221") + .withMetadata("dc", "test", "collection121field", "collection121field-item221") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-item221") + .withMetadata("dc", "test", "collection211field", "collection211field-item221") + .withMetadata("dc", "test", "collection221field", "collection221field-item221") + .build(); + + Item item222 = ItemBuilder.createItem(context, collection222) + .withMetadata("dc", "contributor", "author", "author-item222") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-item222") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-item222") + .withMetadata("dc", "test", "collection111field", "collection111field-item222") + .withMetadata("dc", "test", "collection121field", "collection121field-item222") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-item222") + .withMetadata("dc", "test", "collection211field", "collection211field-item222") + .withMetadata("dc", "test", "collection221field", "collection221field-item222") + .build(); + + Item mappedItem111222 = ItemBuilder + .createItem(context, collection111) + .withMetadata("dc", "contributor", "author", "author-mappedItem111222") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-mappedItem111222") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-mappedItem111222") + .withMetadata("dc", "test", "collection111field", "collection111field-mappedItem111222") + .withMetadata("dc", "test", "collection121field", "collection121field-mappedItem111222") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-mappedItem111222") + .withMetadata("dc", "test", "collection211field", "collection211field-mappedItem111222") + .withMetadata("dc", "test", "collection221field", "collection221field-mappedItem111222") + .build(); + + + Item mappedItem122211 = ItemBuilder + .createItem(context, collection122) + .withMetadata("dc", "contributor", "author", "author-mappedItem122211") + .withMetadata("dc", "test", "parentcommunity1field", "parentcommunity1field-mappedItem122211") + .withMetadata("dc", "test", "subcommunity11field", "subcommunity11field-mappedItem122211") + .withMetadata("dc", "test", "collection111field", "collection111field-mappedItem122211") + .withMetadata("dc", "test", "collection121field", "collection121field-mappedItem122211") + .withMetadata("dc", "test", "subcommunity21field", "subcommunity21field-mappedItem122211") + .withMetadata("dc", "test", "collection211field", "collection211field-mappedItem122211") + .withMetadata("dc", "test", "collection221field", "collection221field-mappedItem122211") + .build(); + + + collectionService.addItem(context, collection222, mappedItem111222); + collectionService.addItem(context, collection211, mappedItem122211); + + + context.dispatchEvents(); + context.restoreAuthSystemState(); + } + + @Test + public void ScopeBasedIndexingAndSearchTestParentCommunity1() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(community1.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("parentcommunity1field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/parentcommunity1field") + .param("scope", String.valueOf(community1.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-item111", 1), + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-item112", 1), + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-item121", 1), + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-item122", 1), + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-mappedItem111222", + 1), + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-mappedItem122211", 1) + ) + )); + + + } + + @Test + public void ScopeBasedIndexingAndSearchTestSubCommunity11() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(subcommunity11.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("subcommunity11field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/subcommunity11field") + .param("scope", String.valueOf(subcommunity11.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("subcommunity11field", + "subcommunity11field-item111", 1), + FacetValueMatcher.matchEntry("subcommunity11field", + "subcommunity11field-item112", 1), + FacetValueMatcher.matchEntry("subcommunity11field", + "subcommunity11field-mappedItem111222", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestCollection111() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection111.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("collection111field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/collection111field") + .param("scope", String.valueOf(collection111.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("collection111field", + "collection111field-item111", 1), + FacetValueMatcher.matchEntry("collection111field", + "collection111field-mappedItem111222", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestCollection112() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection112.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("subcommunity11field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/subcommunity11field") + .param("scope", String.valueOf(collection112.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("subcommunity11field", + "subcommunity11field-item112", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestSubcommunity12() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(subcommunity12.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("parentcommunity1field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/parentcommunity1field") + .param("scope", String.valueOf(subcommunity12.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-item121", 1), + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-item122", 1), + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-mappedItem122211", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestCollection121() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection121.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("collection121field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/collection121field") + .param("scope", String.valueOf(collection121.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("collection121field", + "collection121field-item121", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestCollection122() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection122.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("parentcommunity1field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/parentcommunity1field") + .param("scope", String.valueOf(collection122.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-item122", 1), + FacetValueMatcher.matchEntry("parentcommunity1field", + "parentcommunity1field-mappedItem122211", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestParentCommunity2() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(community2.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.subjectFacet(false), + FacetEntryMatcher.dateIssuedFacet(false), + FacetEntryMatcher.hasContentInOriginalBundleFacet(false), + FacetEntryMatcher.entityTypeFacet(false) + )) + ); + } + + @Test + public void ScopeBasedIndexingAndSearchTestSubCommunity21() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(subcommunity21.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("subcommunity21field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/subcommunity21field") + .param("scope", String.valueOf(subcommunity21.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("subcommunity21field", + "subcommunity21field-item211", 1), + FacetValueMatcher.matchEntry("subcommunity21field", + "subcommunity21field-item212", 1), + FacetValueMatcher.matchEntry("subcommunity21field", + "subcommunity21field-mappedItem122211", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestCollection211() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection211.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("collection211field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/collection211field") + .param("scope", String.valueOf(collection211.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("collection211field", + "collection211field-item211", 1), + FacetValueMatcher.matchEntry("collection211field", + "collection211field-mappedItem122211", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestCollection212() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection212.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("subcommunity21field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/subcommunity21field") + .param("scope", String.valueOf(collection212.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("subcommunity21field", + "subcommunity21field-item212", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestSubcommunity22() throws Exception { + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(subcommunity22.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.subjectFacet(false), + FacetEntryMatcher.dateIssuedFacet(false), + FacetEntryMatcher.hasContentInOriginalBundleFacet(false), + FacetEntryMatcher.entityTypeFacet(false) + )) + ); + } + + @Test + public void ScopeBasedIndexingAndSearchTestCollection221() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection221.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.matchFacet("collection221field", "text", false))) + ); + + getClient().perform(get("/api/discover/facets/collection221field") + .param("scope", String.valueOf(collection221.getID()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._embedded.values", + containsInAnyOrder( + FacetValueMatcher.matchEntry("collection221field", + "collection221field-item221", 1) + ) + )); + } + + @Test + public void ScopeBasedIndexingAndSearchTestCollection222() throws Exception { + + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection222.getID()))) + + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("discover"))) + .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) + .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( + FacetEntryMatcher.authorFacet(false), + FacetEntryMatcher.subjectFacet(false), + FacetEntryMatcher.dateIssuedFacet(false), + FacetEntryMatcher.hasContentInOriginalBundleFacet(false), + FacetEntryMatcher.entityTypeFacet(false) + )) + ); + } + + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetEntryMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetEntryMatcher.java index 5e3c477506..34b7b8b30d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetEntryMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetEntryMatcher.java @@ -99,6 +99,17 @@ public class FacetEntryMatcher { ); } + public static Matcher matchFacet(String name, String facetType, boolean hasNext) { + return allOf( + hasJsonPath("$.name", is(name)), + hasJsonPath("$.facetType", is(facetType)), + hasJsonPath("$.facetLimit", any(Integer.class)), + hasJsonPath("$._links.self.href", containsString("api/discover/facets/" + name)), + hasJsonPath("$._links", matchNextLink(hasNext, "api/discover/facets/" + name)) + ); + } + + /** * Check that a facet over the dc.type exists and match the default configuration * diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetValueMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetValueMatcher.java index a68356da53..1efafb5406 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetValueMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetValueMatcher.java @@ -52,6 +52,16 @@ public class FacetValueMatcher { ); } + public static Matcher matchEntry(String facet, String label, int count) { + return allOf( + hasJsonPath("$.label", is(label)), + hasJsonPath("$.type", is("discover")), + hasJsonPath("$.count", is(count)), + hasJsonPath("$._links.search.href", containsString("api/discover/search/objects")), + hasJsonPath("$._links.search.href", containsString("f." + facet + "=" + label + ",equals")) + ); + } + public static Matcher entrySubject(String label, String authority, int count) { return allOf( diff --git a/machine.cfg b/machine.cfg new file mode 100644 index 0000000000..14f0d1d0b0 --- /dev/null +++ b/machine.cfg @@ -0,0 +1,19 @@ +dspace.shortname = or-platform-7 + +dspace.dir=/Users/yana/dspaces/or-platform-7 + +dspace.server.url =http://localhost:8080/server-or7 +dspace.ui.url = http://localhost:4000 + +# URL for connecting to database +# * Postgres template: jdbc:postgrook naar de toekomst toe wilt dat zeggen dat de backend gewoon in orde is en mogelijk enkel nog eesql://localhost:5432/dspace +# * Oracle template: jdbc:oracle:thin:@//localhost:1521/xe +#db.url = ${db.url} +#db.url = jdbc:postgresql://localhost:5432/or-platform-7 +db.url = jdbc:postgresql://localhost:5434/or-platform-7-4 + + + +solr.server = http://localhost:8983/solr + + From c538b9cbedd2d7ab7ab88b912a5eeb75a180e10d Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 13 Dec 2022 14:27:29 +0100 Subject: [PATCH 06/63] Add docs and remove unused site configuration --- .../org/dspace/discovery/SearchUtils.java | 19 ++- .../DiscoveryConfigurationService.java | 15 ++ .../config/spring/api/discovery.xml | 133 +----------------- .../DiscoveryScopeBasedRestControllerIT.java | 56 ++++++-- dspace/config/spring/api/discovery.xml | 129 ----------------- 5 files changed, 77 insertions(+), 275 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java b/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java index 4085e1bbdf..418720be4a 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java @@ -77,6 +77,7 @@ public class SearchUtils { * * * @param context + * the database context * @param prefix * the namespace of the configuration to lookup if any * @param dso @@ -92,6 +93,15 @@ public class SearchUtils { } } + /** + * Retrieve the configuration for the current dspace object and all its parents and add it to the provided set + * @param context - The database context + * @param configurations - The set of configurations to add the retrieved configurations to + * @param prefix - The namespace of the configuration to lookup if any + * @param dso - The DSpace Object + * @return the set of configurations with additional retrieved ones for the dspace object and parents + * @throws SQLException + */ public static Set addDiscoveryConfigurationForParents( Context context, Set configurations, String prefix, DSpaceObject dso) throws SQLException { @@ -124,6 +134,13 @@ public class SearchUtils { return configurationService.getDiscoveryConfiguration(configurationName); } + + /** + * Return the discovery configuration for the provided DSO + * @param context - The database context + * @param dso - The DSpace object to retrieve the configuration for + * @return the discovery configuration for the provided DSO + */ public static DiscoveryConfiguration getDiscoveryConfigurationByDSO( Context context, DSpaceObject dso) { DiscoveryConfigurationService configurationService = getConfigurationService(); @@ -145,7 +162,7 @@ public class SearchUtils { * A configuration object can be returned for each parent community/collection * * @param item the DSpace item - * @param context + * @param context the database context * @return a list of configuration objects * @throws SQLException An exception that provides information on a database access error or other errors. */ 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 22443aec22..c0eba58669 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 @@ -63,6 +63,13 @@ public class DiscoveryConfigurationService { return getDiscoveryConfiguration(name); } + /** + * Retrieve the discovery configuration for the provided DSO. When no direct match is found, the parent object will + * be checked until there is no parent left, in which case the "default" configuration will be returned. + * @param context - The database context + * @param dso - The DSpace object to retrieve the configuration for + * @return the discovery configuration for the provided DSO. + */ public DiscoveryConfiguration getDiscoveryDSOConfiguration(final Context context, DSpaceObject dso) { String name; @@ -91,6 +98,14 @@ public class DiscoveryConfigurationService { return getDiscoveryConfiguration(name, true); } + /** + * Retrieve the configuration for the provided name. When useDefault is set to true, the "default" configuration + * will be returned when no match is found. When useDefault is set to false, null will be returned when no match is + * found. + * @param name - The name of the configuration to retrieve + * @param useDefault - Whether the default configuration should be used when no match is found + * @return the configuration for the provided name + */ public DiscoveryConfiguration getDiscoveryConfiguration(final String name, boolean useDefault) { DiscoveryConfiguration result; 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 6ffcbe661c..e029c65aa0 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 @@ -50,9 +50,6 @@ --> - - - @@ -77,6 +74,7 @@ + @@ -543,121 +541,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java index a0edf1a0c7..15c1019584 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java @@ -29,12 +29,42 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +/** + * This class tests the correct inheritance of Discovery configurations for sub communities and collections. + * To thoroughly test this, a community and collection structure is set up to where different communities have custom + * configurations configured for them. + * + * The following structure is uses: + * - Parent Community 1 - Custom configuration: discovery-parent-community-1 + * -- Subcommunity 11 - Custom configuration: discovery-sub-community-1-1 + * -- Collection 111 - Custom configuration: discovery-collection-1-1-1 + * -- Collection 112 + * -- Subcommunity 12 + * -- Collection 121 - Custom configuration: discovery-collection-1-2-1 + * -- Collection 122 + * - Parent Community 2 + * -- Subcommunity 21 - Custom configuration: discovery-sub-community-2-1 + * -- Collection 211 - Custom configuration: discovery-collection-2-1-1 + * -- Collection 212 + * -- Subcommunity 22 + * -- Collection 221 - Custom configuration: discovery-collection-2-2-1 + * -- Collection 222 + * + * Each custom configuration contains a unique index for a unique metadata field, to verify if correct information is + * indexed and provided for the different search scopes. + * + * Each collection has an item in it. Next to these items, there are two mapped items, one in collection 111 and 222, + * and one in collection 122 and 211. + * + * The tests will verify that for each object, the correct facets are provided and that all the necessary fields to + * power these facets are indexed properly. + */ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerIntegrationTest { @Autowired CollectionService collectionService; - private Community community1; + private Community parentCommunity1; private Community subcommunity11; private Community subcommunity12; private Collection collection111; @@ -42,7 +72,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg private Collection collection121; private Collection collection122; - private Community community2; + private Community parentCommunity2; private Community subcommunity21; private Community subcommunity22; private Collection collection211; @@ -64,13 +94,13 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg MetadataFieldBuilder.createMetadataField(context, "test", "collection211field", "").build(); MetadataFieldBuilder.createMetadataField(context, "test", "collection221field", "").build(); - community1 = CommunityBuilder.createCommunity(context, "123456789/discovery-parent-community-1") - .build(); + parentCommunity1 = CommunityBuilder.createCommunity(context, "123456789/discovery-parent-community-1") + .build(); subcommunity11 = CommunityBuilder - .createSubCommunity(context, community1, "123456789/discovery-sub-community-1-1") + .createSubCommunity(context, parentCommunity1, "123456789/discovery-sub-community-1-1") .build(); subcommunity12 = CommunityBuilder - .createSubCommunity(context, community1, "123456789/discovery-sub-community-1-2") + .createSubCommunity(context, parentCommunity1, "123456789/discovery-sub-community-1-2") .build(); collection111 = CollectionBuilder .createCollection(context, subcommunity11, "123456789/discovery-collection-1-1-1") @@ -86,15 +116,15 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .createCollection(context, subcommunity12, "123456789/discovery-collection-1-2-2") .build(); - community2 = CommunityBuilder.createCommunity(context, "123456789/discovery-parent-community-2") - .build(); + parentCommunity2 = CommunityBuilder.createCommunity(context, "123456789/discovery-parent-community-2") + .build(); subcommunity21 = CommunityBuilder - .createSubCommunity(context, community2, "123456789/discovery-sub-community-2-1") + .createSubCommunity(context, parentCommunity2, "123456789/discovery-sub-community-2-1") .build(); subcommunity22 = CommunityBuilder - .createSubCommunity(context, community2, "123456789/discovery-sub-community-2-2") + .createSubCommunity(context, parentCommunity2, "123456789/discovery-sub-community-2-2") .build(); collection211 = CollectionBuilder .createCollection(context, subcommunity21, "123456789/discovery-collection-2-1-1") @@ -235,7 +265,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg @Test public void ScopeBasedIndexingAndSearchTestParentCommunity1() throws Exception { - getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(community1.getID()))) + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(parentCommunity1.getID()))) .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("discover"))) @@ -246,7 +276,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg ); getClient().perform(get("/api/discover/facets/parentcommunity1field") - .param("scope", String.valueOf(community1.getID()))) + .param("scope", String.valueOf(parentCommunity1.getID()))) .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("discover"))) .andExpect(jsonPath("$._embedded.values", @@ -435,7 +465,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg @Test public void ScopeBasedIndexingAndSearchTestParentCommunity2() throws Exception { - getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(community2.getID()))) + getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(parentCommunity2.getID()))) .andExpect(status().isOk()) .andExpect(jsonPath("$.type", is("discover"))) diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 4392e02cb3..ae1992fbff 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -50,9 +50,6 @@ --> - - - @@ -534,120 +531,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 463edac869855150b3bb1c6e2f31c8a97482a633 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 13 Dec 2022 17:08:02 +0100 Subject: [PATCH 07/63] Remove local file --- machine.cfg | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 machine.cfg diff --git a/machine.cfg b/machine.cfg deleted file mode 100644 index 14f0d1d0b0..0000000000 --- a/machine.cfg +++ /dev/null @@ -1,19 +0,0 @@ -dspace.shortname = or-platform-7 - -dspace.dir=/Users/yana/dspaces/or-platform-7 - -dspace.server.url =http://localhost:8080/server-or7 -dspace.ui.url = http://localhost:4000 - -# URL for connecting to database -# * Postgres template: jdbc:postgrook naar de toekomst toe wilt dat zeggen dat de backend gewoon in orde is en mogelijk enkel nog eesql://localhost:5432/dspace -# * Oracle template: jdbc:oracle:thin:@//localhost:1521/xe -#db.url = ${db.url} -#db.url = jdbc:postgresql://localhost:5432/or-platform-7 -db.url = jdbc:postgresql://localhost:5434/or-platform-7-4 - - - -solr.server = http://localhost:8983/solr - - From 14534b4eafb8f5333440a624f07395b2cb2f14eb Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Tue, 13 Dec 2022 17:47:35 +0100 Subject: [PATCH 08/63] Move context to first argument in getDiscoveryConfigurationByNameOrDso --- .../configuration/DiscoveryConfigurationService.java | 4 ++-- .../app/rest/repository/DiscoveryRestRepository.java | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) 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 c0eba58669..d7bc3b0f35 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 @@ -119,8 +119,8 @@ public class DiscoveryConfigurationService { return result; } - public DiscoveryConfiguration getDiscoveryConfigurationByNameOrDso(final String configurationName, - final Context context, + public DiscoveryConfiguration getDiscoveryConfigurationByNameOrDso(final Context context, + final String configurationName, final IndexableObject dso) { if (StringUtils.isNotBlank(configurationName) && getMap().containsKey(configurationName)) { return getMap().get(configurationName); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java index 1962d44162..e337e76ef2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java @@ -84,7 +84,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); + .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); return discoverConfigurationConverter.convert(discoveryConfiguration, utils.obtainProjection()); } @@ -96,7 +96,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { Context context = obtainContext(); IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); + .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); DiscoverResult searchResult = null; DiscoverQuery discoverQuery = null; @@ -121,7 +121,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); + .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); return discoverFacetConfigurationConverter.convert(configuration, dsoScope, discoveryConfiguration); } @@ -138,7 +138,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); + .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); DiscoverQuery discoverQuery = queryBuilder.buildFacetQuery(context, scopeObject, discoveryConfiguration, prefix, query, searchFilters, dsoTypes, page, facetName); @@ -157,7 +157,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { Pageable page = PageRequest.of(1, 1); IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(configuration, context, scopeObject); + .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); DiscoverResult searchResult = null; DiscoverQuery discoverQuery = null; From 38b30c394c982c4760a8afc9676bfbe139de5e10 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 14 Dec 2022 10:32:54 +0100 Subject: [PATCH 09/63] Fix openSearchController issue --- .../src/main/java/org/dspace/app/rest/OpenSearchController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 79ca381753..665504139c 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 @@ -176,7 +176,7 @@ public class OpenSearchController { if (dsoObject != null) { container = scopeResolver.resolveScope(context, dsoObject); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso("site", container); + .getDiscoveryConfiguration(context, container); queryArgs.setDiscoveryConfigurationName(discoveryConfiguration.getId()); queryArgs.addFilterQueries(discoveryConfiguration.getDefaultFilterQueries() .toArray( From 69500ad5d579f6891bbf35c35e29b18f120b20e9 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 15 Dec 2022 11:55:05 +0100 Subject: [PATCH 10/63] Fix discovery test config and make ids for relationship profiles unique --- .../config/spring/api/discovery.xml | 733 +++++++++++++++++- dspace/config/spring/api/discovery.xml | 14 +- 2 files changed, 701 insertions(+), 46 deletions(-) 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 e029c65aa0..a5d7682d4c 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 @@ -48,12 +48,15 @@ the key is used to refer to the page (the "site" or a community/collection handle) the value-ref is a reference to an identifier of the DiscoveryConfiguration format --> - - - - - - + + + + + + + + + @@ -61,17 +64,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -176,7 +210,145 @@ + (search.resourcetype:Item AND latestVersion:true) OR search.resourcetype:Collection OR search.resourcetype:Community + -withdrawn:true AND -discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + -withdrawn:true AND -discoverable:false @@ -313,7 +485,7 @@ - search.resourcetype:Item + search.resourcetype:Item AND latestVersion:true withdrawn:true OR discoverable:false @@ -455,7 +627,7 @@ - search.resourcetype:Item + search.resourcetype:Item AND latestVersion:true @@ -541,10 +713,11 @@ + + class="org.dspace.discovery.configuration.DiscoveryConfiguration" + scope="prototype"> @@ -579,7 +752,7 @@ - search.resourcetype:Item OR search.resourcetype:WorkspaceItem OR search.resourcetype:XmlWorkflowItem + (search.resourcetype:Item AND latestVersion:true) OR search.resourcetype:WorkspaceItem OR search.resourcetype:XmlWorkflowItem @@ -616,8 +789,8 @@ + class="org.dspace.discovery.configuration.DiscoveryConfiguration" + scope="prototype"> @@ -691,8 +864,8 @@ + class="org.dspace.discovery.configuration.DiscoveryConfiguration" + scope="prototype"> @@ -814,7 +987,79 @@ + search.resourcetype:Item AND latestVersion:true AND entityType_keyword:Publication + -withdrawn:true AND -discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:Publication + -withdrawn:true AND -discoverable:false @@ -875,7 +1120,71 @@ + search.resourcetype:Item AND latestVersion:true AND entityType_keyword:Person + -withdrawn:true AND -discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:Person + -withdrawn:true AND -discoverable:false @@ -928,7 +1237,63 @@ + search.resourcetype:Item AND latestVersion:true AND entityType_keyword:Project + -withdrawn:true AND -discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:Project + -withdrawn:true AND -discoverable:false @@ -990,7 +1355,73 @@ + search.resourcetype:Item AND latestVersion:true AND entityType_keyword:OrgUnit + -withdrawn:true AND -discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:OrgUnit + -withdrawn:true AND -discoverable:false @@ -1049,7 +1480,69 @@ + search.resourcetype:Item AND latestVersion:true AND entityType_keyword:JournalIssue + -withdrawn:true AND -discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:JournalIssue + -withdrawn:true AND -discoverable:false @@ -1107,7 +1600,68 @@ + search.resourcetype:Item AND latestVersion:true AND entityType_keyword:JournalVolume + -withdrawn:true AND -discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:JournalVolume + -withdrawn:true AND -discoverable:false @@ -1165,7 +1719,68 @@ + search.resourcetype:Item AND latestVersion:true AND entityType_keyword:Journal + -withdrawn:true AND -discoverable:false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item AND entityType_keyword:Journal + -withdrawn:true AND -discoverable:false @@ -1238,7 +1853,8 @@ - search.resourcetype:Item AND (entityType_keyword:OrgUnit OR entityType_keyword:Person) + search.resourcetype:Item AND latestVersion:true AND (entityType_keyword:OrgUnit OR entityType_keyword:Person) + -withdrawn:true AND -discoverable:false @@ -1293,7 +1909,8 @@ - search.resourcetype:Item AND entityType_keyword:OrgUnit AND dc.type:FundingOrganization + search.resourcetype:Item AND latestVersion:true AND entityType_keyword:OrgUnit AND dc.type:FundingOrganization + -withdrawn:true AND -discoverable:false @@ -1302,6 +1919,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item + search.entitytype:${researcher-profile.entity-type:Person} + -withdrawn:true AND -discoverable:false + + + + + + + + + @@ -2288,7 +2943,7 @@ - relation.isAuthorOfPublication + relation.isAuthorOfPublication.latestForDiscovery @@ -2299,7 +2954,7 @@ - relation.isProjectOfPublication + relation.isProjectOfPublication.latestForDiscovery @@ -2311,7 +2966,7 @@ - relation.isOrgUnitOfPublication + relation.isOrgUnitOfPublication.latestForDiscovery @@ -2322,7 +2977,7 @@ - relation.isPublicationOfJournalIssue + relation.isPublicationOfJournalIssue.latestForDiscovery @@ -2333,7 +2988,7 @@ - relation.isJournalOfPublication + relation.isJournalOfPublication.latestForDiscovery @@ -2539,7 +3194,7 @@ - relation.isOrgUnitOfPerson + relation.isOrgUnitOfPerson.latestForDiscovery @@ -2550,7 +3205,7 @@ - relation.isProjectOfPerson + relation.isProjectOfPerson.latestForDiscovery @@ -2562,7 +3217,7 @@ - relation.isPublicationOfAuthor + relation.isPublicationOfAuthor.latestForDiscovery @@ -2634,7 +3289,7 @@ - relation.isPersonOfOrgUnit + relation.isPersonOfOrgUnit.latestForDiscovery @@ -2645,7 +3300,7 @@ - relation.isProjectOfOrgUnit + relation.isProjectOfOrgUnit.latestForDiscovery @@ -2657,7 +3312,7 @@ - relation.isPublicationOfOrgUnit + relation.isPublicationOfOrgUnit.latestForDiscovery @@ -2711,7 +3366,7 @@ - relation.isPublicationOfJournalIssue + relation.isPublicationOfJournalIssue.latestForDiscovery @@ -2734,7 +3389,7 @@ - relation.isIssueOfJournalVolume + relation.isIssueOfJournalVolume.latestForDiscovery @@ -2745,7 +3400,7 @@ - relation.isJournalOfVolume + relation.isJournalOfVolume.latestForDiscovery @@ -2786,7 +3441,7 @@ - relation.isVolumeOfJournal + relation.isVolumeOfJournal.latestForDiscovery @@ -2811,7 +3466,7 @@ - relation.isOrgUnitOfProject + relation.isOrgUnitOfProject.latestForDiscovery @@ -2823,7 +3478,7 @@ - relation.isPersonOfProject + relation.isPersonOfProject.latestForDiscovery @@ -2835,7 +3490,7 @@ - relation.isPublicationOfProject + relation.isPublicationOfProject.latestForDiscovery @@ -2846,7 +3501,7 @@ - relation.isContributorOfPublication + relation.isContributorOfPublication.latestForDiscovery @@ -2857,7 +3512,7 @@ - relation.isPublicationOfContributor + relation.isPublicationOfContributor.latestForDiscovery @@ -2868,7 +3523,7 @@ - relation.isFundingAgencyOfProject + relation.isFundingAgencyOfProject.latestForDiscovery @@ -2879,7 +3534,7 @@ - relation.isProjectOfFundingAgency + relation.isProjectOfFundingAgency.latestForDiscovery diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 5e2cae5e9f..37d5f2548a 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -996,7 +996,7 @@ - + @@ -1129,7 +1129,7 @@ - + @@ -1246,7 +1246,7 @@ - + @@ -1366,7 +1366,7 @@ - + @@ -1491,7 +1491,7 @@ - + @@ -1611,7 +1611,7 @@ - + @@ -1730,7 +1730,7 @@ - + From 1300cdc75b25181fdeebda20661aaa02b2d92bfc Mon Sep 17 00:00:00 2001 From: Yury Bondarenko Date: Mon, 19 Dec 2022 11:20:53 +0100 Subject: [PATCH 11/63] 97248: Cache discovery configurations by UUID --- .../DiscoveryConfigurationService.java | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) 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 d7bc3b0f35..7d5b435555 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 @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -34,6 +35,12 @@ public class DiscoveryConfigurationService { private Map map; private Map> toIgnoreMetadataFields = new HashMap<>(); + /** + * Discovery configurations, cached by DSO UUID. When a DSO doesn't have its own configuration, we take the one of + * the first parent that does. This cache ensures we don't have to go up the hierarchy every time. + */ + private final Map uuidMap = new HashMap<>(); + public Map getMap() { return map; } @@ -72,26 +79,38 @@ public class DiscoveryConfigurationService { */ public DiscoveryConfiguration getDiscoveryDSOConfiguration(final Context context, DSpaceObject dso) { - String name; + // Fall back to default configuration if (dso == null) { - name = "default"; - } else { - name = dso.getHandle(); + return getDiscoveryConfiguration("default", false); } - DiscoveryConfiguration configuration = getDiscoveryConfiguration(name, false); - if (configuration != null) { - return configuration; + // Attempt to retrieve cached configuration by UUID + if (uuidMap.containsKey(dso.getID())) { + return uuidMap.get(dso.getID()); } - DSpaceObjectService dSpaceObjectService = + + DiscoveryConfiguration configuration; + + // Attempt to retrieve configuration by DSO handle + configuration = getDiscoveryConfiguration(dso.getHandle(), false); + + if (configuration == null) { + // Recurse up the Comm/Coll hierarchy until a configuration is found + DSpaceObjectService dSpaceObjectService = ContentServiceFactory.getInstance().getDSpaceObjectService(dso); - DSpaceObject parentObject = null; - try { - parentObject = dSpaceObjectService.getParentObject(context, dso); - } catch (SQLException e) { - log.error(e); + DSpaceObject parentObject = null; + try { + parentObject = dSpaceObjectService.getParentObject(context, dso); + } catch (SQLException e) { + log.error(e); + } + configuration = getDiscoveryDSOConfiguration(context, parentObject); } - return getDiscoveryDSOConfiguration(context, parentObject); + + // Cache the resulting configuration + uuidMap.put(dso.getID(), configuration); + + return configuration; } public DiscoveryConfiguration getDiscoveryConfiguration(final String name) { From a11ed8a0d3f778f8e937512e726d71e28b577349 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Thu, 30 Mar 2023 15:38:09 +0200 Subject: [PATCH 12/63] 100553: Sort the queried metadata fields ASC to always display exact matches on top (this can otherwise lead to angular errors) --- .../discovery/indexobject/MetadataFieldIndexFactoryImpl.java | 1 + .../dspace/app/rest/repository/MetadataFieldRestRepository.java | 1 + 2 files changed, 2 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/MetadataFieldIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/MetadataFieldIndexFactoryImpl.java index 518a8ff145..bef44326fe 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/MetadataFieldIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/MetadataFieldIndexFactoryImpl.java @@ -64,6 +64,7 @@ public class MetadataFieldIndexFactoryImpl extends IndexFactoryImpl Date: Thu, 30 Mar 2023 15:42:18 +0200 Subject: [PATCH 13/63] 100553: Fixed the pagination for core/metadatafield/byFieldName rest endpoint --- .../rest/repository/MetadataFieldRestRepository.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java index b5d12f1d45..65e50005b5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java @@ -135,13 +135,14 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository matchingMetadataFields = new ArrayList<>(); if (StringUtils.isBlank(exactName)) { // Find matches in Solr Search core DiscoverQuery discoverQuery = - this.createDiscoverQuery(context, schemaName, elementName, qualifierName, query); + this.createDiscoverQuery(context, schemaName, elementName, qualifierName, query, pageable); try { DiscoverResult searchResult = searchService.search(context, null, discoverQuery); for (IndexableObject object : searchResult.getIndexableObjects()) { @@ -149,6 +150,7 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository filterQueries = new ArrayList<>(); if (StringUtils.isNotBlank(query)) { if (query.split("\\.").length > 3) { @@ -211,6 +214,8 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository Date: Thu, 30 Mar 2023 17:26:29 +0200 Subject: [PATCH 14/63] 100553: Added backend validation on schema, element and qualifier to check if they contain dots --- .../app/rest/repository/MetadataFieldRestRepository.java | 4 ++++ .../app/rest/repository/MetadataSchemaRestRepository.java | 2 ++ 2 files changed, 6 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java index 65e50005b5..0396a8ad67 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java @@ -253,10 +253,14 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository Date: Mon, 3 Apr 2023 13:05:50 +0200 Subject: [PATCH 15/63] 100553: Removed possibility to updated schema name, element and qualifier --- .../MetadataFieldRestRepository.java | 20 +++-- .../MetadataSchemaRestRepository.java | 15 ++-- .../rest/MetadataSchemaRestRepositoryIT.java | 30 ++++++- .../rest/MetadatafieldRestRepositoryIT.java | 86 +++++++++++++++---- 4 files changed, 114 insertions(+), 37 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java index 0396a8ad67..c185e83342 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java @@ -313,21 +313,23 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository Date: Wed, 5 Apr 2023 13:20:24 +0200 Subject: [PATCH 16/63] 97248: Implement feedback --- .../org/dspace/discovery/SearchUtils.java | 22 +- .../discovery/SolrServiceFileInfoPlugin.java | 8 +- .../DiscoveryConfigurationService.java | 75 +- .../InprogressSubmissionIndexFactoryImpl.java | 2 +- .../indexobject/ItemIndexFactoryImpl.java | 2 +- .../repository/DiscoveryRestRepository.java | 10 +- .../config/spring/api/discovery.xml | 3067 ----------------- .../DiscoveryScopeBasedRestControllerIT.java | 22 +- .../app/rest/matcher/FacetEntryMatcher.java | 2 +- 9 files changed, 95 insertions(+), 3115 deletions(-) delete mode 100644 dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml diff --git a/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java b/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java index 418720be4a..4a53e34454 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SearchUtils.java @@ -62,12 +62,24 @@ public class SearchUtils { return searchService; } + /** + * Retrieves the Discovery Configuration for a null context, prefix and DSpace object. + * This will result in returning the default configuration + * @return the default configuration + */ public static DiscoveryConfiguration getDiscoveryConfiguration() { return getDiscoveryConfiguration(null, null, null); } - public static DiscoveryConfiguration getDiscoveryConfiguration(final Context context, - DSpaceObject dso) { + /** + * Retrieves the Discovery Configuration with a null prefix for a DSpace object. + * @param context + * the dabase context + * @param dso + * the DSpace object + * @return the Discovery Configuration for the specified DSpace object + */ + public static DiscoveryConfiguration getDiscoveryConfiguration(Context context, DSpaceObject dso) { return getDiscoveryConfiguration(context, null, dso); } @@ -84,7 +96,7 @@ public class SearchUtils { * the DSpaceObject * @return the discovery configuration for the specified scope */ - public static DiscoveryConfiguration getDiscoveryConfiguration(final Context context, String prefix, + public static DiscoveryConfiguration getDiscoveryConfiguration(Context context, String prefix, DSpaceObject dso) { if (prefix != null) { return getDiscoveryConfigurationByName(dso != null ? prefix + "." + dso.getHandle() : prefix); @@ -161,12 +173,12 @@ public class SearchUtils { * Method that retrieves a list of all the configuration objects from the given item * A configuration object can be returned for each parent community/collection * - * @param item the DSpace item * @param context the database context + * @param item the DSpace item * @return a list of configuration objects * @throws SQLException An exception that provides information on a database access error or other errors. */ - public static List getAllDiscoveryConfigurations(Item item, Context context) + public static List getAllDiscoveryConfigurations(Context context, Item item) throws SQLException { List collections = item.getCollections(); return getAllDiscoveryConfigurations(context, null, collections, item); 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 6bda2fc52d..7aece5acf3 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -53,16 +53,20 @@ public class SolrServiceFileInfoPlugin implements SolrServiceIndexPlugin { 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()); 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", - bitstream.getName()); + description); document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter", - bitstream.getName()); + description); } } } 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 7d5b435555..da23b87a35 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 @@ -17,6 +17,8 @@ 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.content.Collection; +import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.DSpaceObjectService; @@ -36,10 +38,11 @@ public class DiscoveryConfigurationService { private Map> toIgnoreMetadataFields = new HashMap<>(); /** - * Discovery configurations, cached by DSO UUID. When a DSO doesn't have its own configuration, we take the one of - * the first parent that does. This cache ensures we don't have to go up the hierarchy every time. + * Discovery configurations, cached by Community/Collection UUID. When a Community or Collection does not have its + * own configuration, we take the one of the first parent that does. + * This cache ensures we do not have to go up the hierarchy every time. */ - private final Map uuidMap = new HashMap<>(); + private final Map comColToDiscoveryConfigurationMap = new HashMap<>(); public Map getMap() { return map; @@ -57,15 +60,26 @@ public class DiscoveryConfigurationService { this.toIgnoreMetadataFields = toIgnoreMetadataFields; } - public DiscoveryConfiguration getDiscoveryConfiguration(final Context context, - IndexableObject dso) { + /** + * Retrieve the discovery configuration for the provided IndexableObject. When a DSpace Object can be retrieved from + * the IndexableObject, the discovery configuration will be returned for the DSpace Object. Otherwise, a check will + * be done to look for the unique index ID of the IndexableObject. When the IndexableObject is null, the default + * configuration will be retrieved + * + * When no direct match is found, the parent object will + * be checked until there is no parent left, in which case the "default" configuration will be returned. + * @param context - The database context + * @param indexableObject - The IndexableObject to retrieve the configuration for + * @return the discovery configuration for the provided IndexableObject. + */ + public DiscoveryConfiguration getDiscoveryConfiguration(Context context, IndexableObject indexableObject) { String name; - if (dso == null) { - name = "default"; - } else if (dso instanceof IndexableDSpaceObject) { - return getDiscoveryDSOConfiguration(context, ((IndexableDSpaceObject) dso).getIndexedObject()); + if (indexableObject == null) { + return getDiscoveryConfiguration(null); + } else if (indexableObject instanceof IndexableDSpaceObject) { + return getDiscoveryDSOConfiguration(context, ((IndexableDSpaceObject) indexableObject).getIndexedObject()); } else { - name = dso.getUniqueIndexID(); + name = indexableObject.getUniqueIndexID(); } return getDiscoveryConfiguration(name); } @@ -77,16 +91,15 @@ public class DiscoveryConfigurationService { * @param dso - The DSpace object to retrieve the configuration for * @return the discovery configuration for the provided DSO. */ - public DiscoveryConfiguration getDiscoveryDSOConfiguration(final Context context, - DSpaceObject dso) { + public DiscoveryConfiguration getDiscoveryDSOConfiguration(final Context context, DSpaceObject dso) { // Fall back to default configuration if (dso == null) { - return getDiscoveryConfiguration("default", false); + return getDiscoveryConfiguration(null, true); } // Attempt to retrieve cached configuration by UUID - if (uuidMap.containsKey(dso.getID())) { - return uuidMap.get(dso.getID()); + if (comColToDiscoveryConfigurationMap.containsKey(dso.getID())) { + return comColToDiscoveryConfigurationMap.get(dso.getID()); } DiscoveryConfiguration configuration; @@ -107,13 +120,21 @@ public class DiscoveryConfigurationService { configuration = getDiscoveryDSOConfiguration(context, parentObject); } - // Cache the resulting configuration - uuidMap.put(dso.getID(), configuration); + // Cache the resulting configuration when the DSO is a Community or Collection + if (dso instanceof Community || dso instanceof Collection) { + comColToDiscoveryConfigurationMap.put(dso.getID(), configuration); + } return configuration; } - public DiscoveryConfiguration getDiscoveryConfiguration(final String name) { + /** + * Retrieve the Discovery Configuration for the provided name. When no configuration can be found for the name, the + * default configuration will be returned. + * @param name - The name of the configuration to be retrieved + * @return the Discovery Configuration for the provided name, or default when none was found. + */ + public DiscoveryConfiguration getDiscoveryConfiguration(String name) { return getDiscoveryConfiguration(name, true); } @@ -138,13 +159,23 @@ public class DiscoveryConfigurationService { return result; } - public DiscoveryConfiguration getDiscoveryConfigurationByNameOrDso(final Context context, - final String configurationName, - final IndexableObject dso) { + /** + * Retrieve the Discovery configuration for the provided name or IndexableObject. The configuration will first be + * checked for the provided name. When no match is found for the name, the configuration will be retrieved for the + * IndexableObject + * + * @param context - The database context + * @param configurationName - The name of the configuration to be retrieved + * @param indexableObject - The indexable object to retrieve the configuration for + * @return the Discovery configuration for the provided name, or when not found for the provided IndexableObject + */ + public DiscoveryConfiguration getDiscoveryConfigurationByNameOrIndexableObject(Context context, + String configurationName, + IndexableObject indexableObject) { if (StringUtils.isNotBlank(configurationName) && getMap().containsKey(configurationName)) { return getMap().get(configurationName); } else { - return getDiscoveryConfiguration(context, dso); + return getDiscoveryConfiguration(context, indexableObject); } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/InprogressSubmissionIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/InprogressSubmissionIndexFactoryImpl.java index ebedfc34b7..04c5e7d432 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/InprogressSubmissionIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/InprogressSubmissionIndexFactoryImpl.java @@ -79,7 +79,7 @@ public abstract class InprogressSubmissionIndexFactoryImpl discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, (WorkspaceItem) inProgressSubmission); } else { - discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(item, context); + discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, item); } indexableItemService.addDiscoveryFields(doc, context, item, discoveryConfigurations); indexableCollectionService.storeCommunityCollectionLocations(doc, locations); diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java index b417237f76..412442fb1c 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java @@ -147,7 +147,7 @@ public class ItemIndexFactoryImpl extends DSpaceObjectIndexFactoryImpl discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(item, context); + List discoveryConfigurations = SearchUtils.getAllDiscoveryConfigurations(context, item); addDiscoveryFields(doc, context, indexableItem.getIndexedObject(), discoveryConfigurations); //mandatory facet to show status on mydspace diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java index e337e76ef2..46c8ab3e39 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DiscoveryRestRepository.java @@ -84,7 +84,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); + .getDiscoveryConfigurationByNameOrIndexableObject(context, configuration, scopeObject); return discoverConfigurationConverter.convert(discoveryConfiguration, utils.obtainProjection()); } @@ -96,7 +96,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { Context context = obtainContext(); IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); + .getDiscoveryConfigurationByNameOrIndexableObject(context, configuration, scopeObject); DiscoverResult searchResult = null; DiscoverQuery discoverQuery = null; @@ -121,7 +121,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); + .getDiscoveryConfigurationByNameOrIndexableObject(context, configuration, scopeObject); return discoverFacetConfigurationConverter.convert(configuration, dsoScope, discoveryConfiguration); } @@ -138,7 +138,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); + .getDiscoveryConfigurationByNameOrIndexableObject(context, configuration, scopeObject); DiscoverQuery discoverQuery = queryBuilder.buildFacetQuery(context, scopeObject, discoveryConfiguration, prefix, query, searchFilters, dsoTypes, page, facetName); @@ -157,7 +157,7 @@ public class DiscoveryRestRepository extends AbstractDSpaceRestRepository { Pageable page = PageRequest.of(1, 1); IndexableObject scopeObject = scopeResolver.resolveScope(context, dsoScope); DiscoveryConfiguration discoveryConfiguration = searchConfigurationService - .getDiscoveryConfigurationByNameOrDso(context, configuration, scopeObject); + .getDiscoveryConfigurationByNameOrIndexableObject(context, configuration, scopeObject); DiscoverResult searchResult = null; DiscoverQuery discoverQuery = null; 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 deleted file mode 100644 index e029c65aa0..0000000000 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/discovery.xml +++ /dev/null @@ -1,3067 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.rights - - - - - - - - - - - - - - - dc.rights - - - - - - - - dc.description.provenance - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item - - withdrawn:true OR discoverable:false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:WorkspaceItem OR search.resourcetype:XmlWorkflowItem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:PoolTask OR search.resourcetype:ClaimedTask - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:XmlWorkflowItem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND entityType_keyword:Publication - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND entityType_keyword:Person - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND entityType_keyword:Project - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND entityType_keyword:OrgUnit - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND entityType_keyword:JournalIssue - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND entityType_keyword:JournalVolume - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND entityType_keyword:Journal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND (entityType_keyword:OrgUnit OR entityType_keyword:Person) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item AND entityType_keyword:OrgUnit AND dc.type:FundingOrganization - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - dc.contributor.author - dc.creator - dc.subject - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - - - - - - - - - - - relation.isAuthorOfPublication - - - - - - - - - - - relation.isProjectOfPublication - - - - - - - - - - - - relation.isOrgUnitOfPublication - - - - - - - - - - - relation.isPublicationOfJournalIssue - - - - - - - - - - - relation.isJournalOfPublication - - - - - - - - - - - dc.contributor.author - dc.creator - - - - - - - - - - - - - - - dspace.entity.type - - - - - - - - - - - - - - dc.subject.* - - - - - - - - - - - - - - dc.date.issued - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.type - - - - - - - - - dc.identifier - - - - - - - - - placeholder.placeholder.placeholder - - - - - - - - - - placeholder.placeholder.placeholder - - - - - - - - - person.jobTitle - - - - - - - - - - - - - - - person.knowsLanguage - - - - - - - - - - - - - person.birthDate - - - - - - - - - - - - - - - - - person.familyName - - - - - - - - - - - person.givenName - - - - - - - - - - - relation.isOrgUnitOfPerson - - - - - - - - - - - relation.isProjectOfPerson - - - - - - - - - - - relation.isPublicationOfAuthor - - - - - - - - - - - - organization.address.addressCountry - - - - - - - - - - - - - - - organization.address.addressLocality - - - - - - - - - - - - - - - organization.foundingDate - - - - - - - - - - - - - - - - organization.legalName - - - - - - - - - - - relation.isPersonOfOrgUnit - - - - - - - - - - - relation.isProjectOfOrgUnit - - - - - - - - - - - relation.isPublicationOfOrgUnit - - - - - - - - - - - creativework.keywords - - - - - - - - - - - - - - - creativework.datePublished - - - - - - - - - - - - - - - - publicationissue.issueNumber - - - - - - - - - - - relation.isPublicationOfJournalIssue - - - - - - - - - - - publicationVolume.volumeNumber - - - - - - - - - - - relation.isIssueOfJournalVolume - - - - - - - - - - - relation.isJournalOfVolume - - - - - - - - - - - creativework.publisher - - - - - - - - - - - - - - - creativework.editor - - - - - - - - - - - - - - - relation.isVolumeOfJournal - - - - - - - - - - - - - - placeholder.placeholder.placeholder - - - - - - - - - - relation.isOrgUnitOfProject - - - - - - - - - - - - relation.isPersonOfProject - - - - - - - - - - - - relation.isPublicationOfProject - - - - - - - - - - - relation.isContributorOfPublication - - - - - - - - - - - relation.isPublicationOfContributor - - - - - - - - - - - relation.isFundingAgencyOfProject - - - - - - - - - - - relation.isProjectOfFundingAgency - - - - - - - - - - - dc.test.parentcommunity1field - - - - - - - - - - - - - - - dc.test.subcommunity11field - - - - - - - - - - - - - - - dc.test.collection111field - - - - - - - - - - - - - - - dc.test.collection121field - - - - - - - - - - - - - - - dc.test.subcommunity21field - - - - - - - - - - - - - - dc.test.collection211field - - - - - - - - - - - - - - dc.test.collection221field - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java index 15c1019584..0c8735545e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java @@ -272,7 +272,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("parentcommunity1field", "text", false))) + FacetEntryMatcher.matchFacet(false, "parentcommunity1field", "text"))) ); getClient().perform(get("/api/discover/facets/parentcommunity1field") @@ -310,7 +310,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("subcommunity11field", "text", false))) + FacetEntryMatcher.matchFacet(false, "subcommunity11field", "text"))) ); getClient().perform(get("/api/discover/facets/subcommunity11field") @@ -339,7 +339,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("collection111field", "text", false))) + FacetEntryMatcher.matchFacet(false, "collection111field", "text"))) ); getClient().perform(get("/api/discover/facets/collection111field") @@ -366,7 +366,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("subcommunity11field", "text", false))) + FacetEntryMatcher.matchFacet(false, "subcommunity11field", "text"))) ); getClient().perform(get("/api/discover/facets/subcommunity11field") @@ -391,7 +391,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("parentcommunity1field", "text", false))) + FacetEntryMatcher.matchFacet(false, "parentcommunity1field", "text"))) ); getClient().perform(get("/api/discover/facets/parentcommunity1field") @@ -420,7 +420,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("collection121field", "text", false))) + FacetEntryMatcher.matchFacet(false, "collection121field", "text"))) ); getClient().perform(get("/api/discover/facets/collection121field") @@ -445,7 +445,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("parentcommunity1field", "text", false))) + FacetEntryMatcher.matchFacet(false, "parentcommunity1field", "text"))) ); getClient().perform(get("/api/discover/facets/parentcommunity1field") @@ -490,7 +490,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("subcommunity21field", "text", false))) + FacetEntryMatcher.matchFacet(false, "subcommunity21field", "text"))) ); getClient().perform(get("/api/discover/facets/subcommunity21field") @@ -519,7 +519,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("collection211field", "text", false))) + FacetEntryMatcher.matchFacet(false, "collection211field", "text"))) ); getClient().perform(get("/api/discover/facets/collection211field") @@ -546,7 +546,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("subcommunity21field", "text", false))) + FacetEntryMatcher.matchFacet(false, "subcommunity21field", "text"))) ); getClient().perform(get("/api/discover/facets/subcommunity21field") @@ -588,7 +588,7 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg .andExpect(jsonPath("$._links.self.href", containsString("api/discover/facets"))) .andExpect(jsonPath("$._embedded.facets", containsInAnyOrder( FacetEntryMatcher.authorFacet(false), - FacetEntryMatcher.matchFacet("collection221field", "text", false))) + FacetEntryMatcher.matchFacet(false, "collection221field", "text"))) ); getClient().perform(get("/api/discover/facets/collection221field") diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetEntryMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetEntryMatcher.java index 34b7b8b30d..60b5f417ed 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetEntryMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/FacetEntryMatcher.java @@ -99,7 +99,7 @@ public class FacetEntryMatcher { ); } - public static Matcher matchFacet(String name, String facetType, boolean hasNext) { + public static Matcher matchFacet(boolean hasNext, String name, String facetType) { return allOf( hasJsonPath("$.name", is(name)), hasJsonPath("$.facetType", is(facetType)), From e433720cd005ddf6b6e13ce09988c72500a74115 Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Wed, 5 Apr 2023 14:07:52 +0200 Subject: [PATCH 17/63] Add test-discovery xml --- .../config/spring/api/test-discovery.xml | 1115 +++++++++++++++++ 1 file changed, 1115 insertions(+) create mode 100644 dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml new file mode 100644 index 0000000000..8b11a87e2d --- /dev/null +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml @@ -0,0 +1,1115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.rights + + + + + + + + + + + + + + + dc.rights + + + + + + + + dc.description.provenance + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + search.resourcetype:Item OR search.resourcetype:Collection OR search.resourcetype:Community + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + dc.contributor.author + dc.creator + dc.subject + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.test.parentcommunity1field + + + + + + + + + + + + + + + dc.test.subcommunity11field + + + + + + + + + + + + + + + dc.test.collection111field + + + + + + + + + + + + + + + dc.test.collection121field + + + + + + + + + + + + + + + dc.test.subcommunity21field + + + + + + + + + + + + + + dc.test.collection211field + + + + + + + + + + + + + + dc.test.collection221field + + + + + + + + + + + From cf831ed7d5892fb86b1569b8c02c406e546e2e8e Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Fri, 7 Apr 2023 16:26:12 +0200 Subject: [PATCH 18/63] Fix merge issues --- dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java | 2 +- .../data/dspaceFolder/config/spring/api/test-discovery.xml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) 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 e02367f6eb..f99aab852b 100644 --- a/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java +++ b/dspace-api/src/main/java/org/dspace/browse/SolrBrowseDAO.java @@ -239,7 +239,7 @@ public class SolrBrowseDAO implements BrowseDAO { } private void addDefaultFilterQueries(DiscoverQuery query) { - DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(container); + DiscoveryConfiguration discoveryConfiguration = SearchUtils.getDiscoveryConfiguration(context, container); discoveryConfiguration.getDefaultFilterQueries().forEach(query::addFilterQueries); } diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml index 8b11a87e2d..4a91ef051e 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml @@ -39,10 +39,13 @@ + + + From 66eb8a548fe55698cd53766fb86f605f23534323 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Thu, 4 May 2023 20:11:47 +0200 Subject: [PATCH 19/63] Browse-by support for controlled vocabularies https://github.com/DSpace/RestContract/pull/225 --- .../authority/ChoiceAuthorityServiceImpl.java | 54 ++++++++++++++ .../DSpaceControlledVocabularyIndex.java | 45 ++++++++++++ .../service/ChoiceAuthorityService.java | 4 + .../DiscoveryConfigurationService.java | 12 +++ .../rest/converter/BrowseIndexConverter.java | 6 +- .../HierarchicalBrowseConverter.java | 42 +++++++++++ .../rest/link/BrowseEntryHalLinkFactory.java | 4 +- .../app/rest/model/BrowseIndexRest.java | 73 +++++++++++++++---- .../model/hateoas/BrowseIndexResource.java | 34 ++++++++- .../repository/BrowseEntryLinkRepository.java | 5 +- .../repository/BrowseIndexRestRepository.java | 30 +++++++- .../repository/BrowseItemLinkRepository.java | 5 +- .../repository/VocabularyRestRepository.java | 2 +- 13 files changed, 286 insertions(+), 30 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabularyIndex.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/HierarchicalBrowseConverter.java 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 f25e2c4646..ec8f8769be 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 @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; @@ -30,6 +31,8 @@ import org.dspace.content.MetadataValue; import org.dspace.content.authority.service.ChoiceAuthorityService; import org.dspace.core.Utils; import org.dspace.core.service.PluginService; +import org.dspace.discovery.configuration.DiscoveryConfigurationService; +import org.dspace.discovery.configuration.DiscoverySearchFilterFacet; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; @@ -80,6 +83,9 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService protected Map>> authoritiesFormDefinitions = new HashMap>>(); + // Map of vocabulary authorities to and their index info equivalent + protected Map vocabularyIndexMap = new HashMap<>(); + // the item submission reader private SubmissionConfigReader itemSubmissionConfigReader; @@ -87,6 +93,8 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService protected ConfigurationService configurationService; @Autowired(required = true) protected PluginService pluginService; + @Autowired + private DiscoveryConfigurationService searchConfigurationService; final static String CHOICES_PLUGIN_PREFIX = "choices.plugin."; final static String CHOICES_PRESENTATION_PREFIX = "choices.presentation."; @@ -540,4 +548,50 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService HierarchicalAuthority ma = (HierarchicalAuthority) getChoiceAuthorityByAuthorityName(authorityName); return ma.getParentChoice(authorityName, vocabularyId, locale); } + + @Override + public DSpaceControlledVocabularyIndex getVocabularyIndex(String nameVocab) { + if (this.vocabularyIndexMap.containsKey(nameVocab)) { + return this.vocabularyIndexMap.get(nameVocab); + } else { + init(); + ChoiceAuthority source = this.getChoiceAuthorityByAuthorityName(nameVocab); + if (source != null && source instanceof DSpaceControlledVocabulary) { + Set metadataFields = new HashSet<>(); + Map> formsToFields = this.authoritiesFormDefinitions.get(nameVocab); + for (Map.Entry> formToField : formsToFields.entrySet()) { + metadataFields.addAll(formToField.getValue().stream().map(value -> + StringUtils.replace(value, "_", ".")) + .collect(Collectors.toList())); + } + DiscoverySearchFilterFacet matchingFacet = null; + for (DiscoverySearchFilterFacet facetConfig : searchConfigurationService.getAllFacetsConfig()) { + boolean coversAllFieldsFromVocab = true; + for (String fieldFromVocab: metadataFields) { + boolean coversFieldFromVocab = false; + for (String facetMdField: facetConfig.getMetadataFields()) { + if (facetMdField.startsWith(fieldFromVocab)) { + coversFieldFromVocab = true; + break; + } + } + if (!coversFieldFromVocab) { + coversAllFieldsFromVocab = false; + break; + } + } + if (coversAllFieldsFromVocab) { + matchingFacet = facetConfig; + break; + } + } + DSpaceControlledVocabularyIndex vocabularyIndex = + new DSpaceControlledVocabularyIndex((DSpaceControlledVocabulary) source, metadataFields, + matchingFacet); + this.vocabularyIndexMap.put(nameVocab, vocabularyIndex); + return vocabularyIndex; + } + return null; + } + } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabularyIndex.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabularyIndex.java new file mode 100644 index 0000000000..6f350fc71e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabularyIndex.java @@ -0,0 +1,45 @@ +/** + * 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.util.Set; + +import org.dspace.discovery.configuration.DiscoverySearchFilterFacet; + +/** + * Helper class to transform a {@link org.dspace.content.authority.DSpaceControlledVocabulary} into a + * {@code BrowseIndexRest} + * cached by {@link org.dspace.content.authority.service.ChoiceAuthorityService#getVocabularyIndex(String)} + * + * @author Marie Verdonck (Atmire) on 04/05/2023 + */ +public class DSpaceControlledVocabularyIndex { + + protected DSpaceControlledVocabulary vocabulary; + protected Set metadataFields; + protected DiscoverySearchFilterFacet facetConfig; + + public DSpaceControlledVocabularyIndex(DSpaceControlledVocabulary controlledVocabulary, Set metadataFields, + DiscoverySearchFilterFacet facetConfig) { + this.vocabulary = controlledVocabulary; + this.metadataFields = metadataFields; + this.facetConfig = facetConfig; + } + + public DSpaceControlledVocabulary getVocabulary() { + return vocabulary; + } + + public Set getMetadataFields() { + return this.metadataFields; + } + + public DiscoverySearchFilterFacet getFacetConfig() { + return this.facetConfig; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/authority/service/ChoiceAuthorityService.java b/dspace-api/src/main/java/org/dspace/content/authority/service/ChoiceAuthorityService.java index eb34de29c1..a9fd24e947 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/service/ChoiceAuthorityService.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/service/ChoiceAuthorityService.java @@ -15,6 +15,7 @@ import org.dspace.content.MetadataValue; import org.dspace.content.authority.Choice; import org.dspace.content.authority.ChoiceAuthority; import org.dspace.content.authority.Choices; +import org.dspace.content.authority.DSpaceControlledVocabularyIndex; /** * Broker for ChoiceAuthority plugins, and for other information configured @@ -220,4 +221,7 @@ public interface ChoiceAuthorityService { * @return the parent Choice object if any */ public Choice getParentChoice(String authorityName, String vocabularyId, String locale); + + public DSpaceControlledVocabularyIndex getVocabularyIndex(String nameVocab); + } 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 636e7ccd2a..f4fd3ca0ef 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 @@ -92,6 +92,18 @@ public class DiscoveryConfigurationService { return configs; } + /** + * @return All configurations for {@link org.dspace.discovery.configuration.DiscoverySearchFilterFacet} + */ + public List getAllFacetsConfig() { + List configs = new ArrayList<>(); + for (String key : map.keySet()) { + DiscoveryConfiguration config = map.get(key); + configs.addAll(config.getSidebarFacets()); + } + return configs; + } + public static void main(String[] args) { System.out.println(DSpaceServicesFactory.getInstance().getServiceManager().getServicesNames().size()); DiscoveryConfigurationService mainService = DSpaceServicesFactory.getInstance().getServiceManager() diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/BrowseIndexConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/BrowseIndexConverter.java index 6ee836e5fc..1e2899b396 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/BrowseIndexConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/BrowseIndexConverter.java @@ -7,6 +7,9 @@ */ package org.dspace.app.rest.converter; +import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_FLAT; +import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_VALUE_LIST; + import java.util.ArrayList; import java.util.List; @@ -33,14 +36,15 @@ public class BrowseIndexConverter implements DSpaceConverter metadataList = new ArrayList(); if (obj.isMetadataIndex()) { for (String s : obj.getMetadata().split(",")) { metadataList.add(s.trim()); } + bir.setBrowseType(BROWSE_TYPE_VALUE_LIST); } else { metadataList.add(obj.getSortOption().getMetadata()); + bir.setBrowseType(BROWSE_TYPE_FLAT); } bir.setMetadataList(metadataList); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/HierarchicalBrowseConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/HierarchicalBrowseConverter.java new file mode 100644 index 0000000000..7b0cea9d8f --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/HierarchicalBrowseConverter.java @@ -0,0 +1,42 @@ +/** + * 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.converter; + +import java.util.ArrayList; + +import org.dspace.app.rest.model.BrowseIndexRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.content.authority.DSpaceControlledVocabularyIndex; +import org.springframework.stereotype.Component; + +/** + * This is the converter from a {@link org.dspace.content.authority.DSpaceControlledVocabularyIndex} to a + * {@link org.dspace.app.rest.model.BrowseIndexRest#BROWSE_TYPE_HIERARCHICAL} {@link org.dspace.app.rest.model.BrowseIndexRest} + * + * @author Marie Verdonck (Atmire) on 04/05/2023 + */ +@Component +public class HierarchicalBrowseConverter implements DSpaceConverter { + + @Override + public BrowseIndexRest convert(DSpaceControlledVocabularyIndex obj, Projection projection) { + BrowseIndexRest bir = new BrowseIndexRest(); + bir.setProjection(projection); + bir.setId(obj.getVocabulary().getPluginInstanceName()); + bir.setBrowseType(BrowseIndexRest.BROWSE_TYPE_HIERARCHICAL); + bir.setFacetType(obj.getFacetConfig().getIndexFieldName()); + bir.setVocabulary(obj.getVocabulary().getPluginInstanceName()); + bir.setMetadataList(new ArrayList<>(obj.getMetadataFields())); + return bir; + } + + @Override + public Class getModelClass() { + return DSpaceControlledVocabularyIndex.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/BrowseEntryHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/BrowseEntryHalLinkFactory.java index ee70dbf431..9e515984fe 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/BrowseEntryHalLinkFactory.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/BrowseEntryHalLinkFactory.java @@ -37,11 +37,11 @@ public class BrowseEntryHalLinkFactory extends HalLinkFactory { public static final String CATEGORY = RestAddressableModel.DISCOVER; - public static final String ITEMS = "items"; - public static final String ENTRIES = "entries"; + public static final String LINK_ITEMS = "items"; + public static final String LINK_ENTRIES = "entries"; + public static final String LINK_VOCABULARY = "vocabulary"; - boolean metadataBrowse; + // if the browse index has two levels, the 1st level shows the list of entries like author names, subjects, types, + // etc. the second level is the actual list of items linked to a specific entry + public static final String BROWSE_TYPE_VALUE_LIST = "valueList"; + // if the browse index has one level: the full list of items + public static final String BROWSE_TYPE_FLAT = "flatBrowse"; + // if the browse index should display the vocabulary tree. The 1st level shows the tree. + // The second level is the actual list of items linked to a specific entry + public static final String BROWSE_TYPE_HIERARCHICAL = "hierarchicalBrowse"; + // Shared fields + String browseType; @JsonProperty(value = "metadata") List metadataList; + // Single browse index fields + @JsonInclude(JsonInclude.Include.NON_NULL) String dataType; - + @JsonInclude(JsonInclude.Include.NON_NULL) List sortOptions; - + @JsonInclude(JsonInclude.Include.NON_NULL) String order; + // Hierarchical browse fields + @JsonInclude(JsonInclude.Include.NON_NULL) + String facetType; + @JsonInclude(JsonInclude.Include.NON_NULL) + String vocabulary; + @JsonIgnore @Override public String getCategory() { @@ -60,14 +79,6 @@ public class BrowseIndexRest extends BaseObjectRest { return NAME; } - public boolean isMetadataBrowse() { - return metadataBrowse; - } - - public void setMetadataBrowse(boolean metadataBrowse) { - this.metadataBrowse = metadataBrowse; - } - public List getMetadataList() { return metadataList; } @@ -100,6 +111,38 @@ public class BrowseIndexRest extends BaseObjectRest { this.sortOptions = sortOptions; } + /** + * - valueList => if the browse index has two levels, the 1st level shows the list of entries like author names, + * subjects, types, etc. the second level is the actual list of items linked to a specific entry + * - flatBrowse if the browse index has one level: the full list of items + * - hierarchicalBrowse if the browse index should display the vocabulary tree. The 1st level shows the tree. + * The second level is the actual list of items linked to a specific entry + */ + public void setBrowseType(String browseType) { + this.browseType = browseType; + } + + public String getBrowseType() { + return browseType; + } + + public void setFacetType(String facetType) { + this.facetType = facetType; + } + + public String getFacetType() { + return facetType; + } + + public void setVocabulary(String vocabulary) { + this.vocabulary = vocabulary; + } + + + public String getVocabulary() { + return vocabulary; + } + @Override public Class getController() { return RestResourceController.class; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/BrowseIndexResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/BrowseIndexResource.java index f6c821595f..61158704ea 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/BrowseIndexResource.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/BrowseIndexResource.java @@ -7,9 +7,20 @@ */ package org.dspace.app.rest.model.hateoas; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + +import org.atteo.evo.inflector.English; +import org.dspace.app.rest.RestResourceController; import org.dspace.app.rest.model.BrowseIndexRest; +import org.dspace.app.rest.model.VocabularyRest; import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; import org.dspace.app.rest.utils.Utils; +import org.dspace.content.authority.ChoiceAuthority; +import org.dspace.content.authority.factory.ContentAuthorityServiceFactory; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.springframework.hateoas.Link; +import org.springframework.web.util.UriComponentsBuilder; /** * Browse Index Rest HAL Resource. The HAL Resource wraps the REST Resource @@ -19,15 +30,32 @@ import org.dspace.app.rest.utils.Utils; */ @RelNameDSpaceResource(BrowseIndexRest.NAME) public class BrowseIndexResource extends DSpaceResource { + + public BrowseIndexResource(BrowseIndexRest bix, Utils utils) { super(bix, utils); // TODO: the following code will force the embedding of items and // entries in the browseIndex we need to find a way to populate the rels // array from the request/projection right now it is always null // super(bix, utils, "items", "entries"); - if (bix.isMetadataBrowse()) { - add(utils.linkToSubResource(bix, BrowseIndexRest.ENTRIES)); + if (bix.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_VALUE_LIST)) { + add(utils.linkToSubResource(bix, BrowseIndexRest.LINK_ENTRIES)); + add(utils.linkToSubResource(bix, BrowseIndexRest.LINK_ITEMS)); + } + if (bix.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_FLAT)) { + add(utils.linkToSubResource(bix, BrowseIndexRest.LINK_ITEMS)); + } + if (bix.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_HIERARCHICAL)) { + ChoiceAuthorityService choiceAuthorityService = + ContentAuthorityServiceFactory.getInstance().getChoiceAuthorityService(); + ChoiceAuthority source = choiceAuthorityService.getChoiceAuthorityByAuthorityName(bix.getVocabulary()); + UriComponentsBuilder baseLink = linkTo( + methodOn(RestResourceController.class, VocabularyRest.AUTHENTICATION).findRel(null, + null, VocabularyRest.CATEGORY, + English.plural(VocabularyRest.NAME), source.getPluginInstanceName(), + "", null, null)).toUriComponentsBuilder(); + + add(Link.of(baseLink.build().encode().toUriString(), BrowseIndexRest.LINK_VOCABULARY)); } - add(utils.linkToSubResource(bix, BrowseIndexRest.ITEMS)); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseEntryLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseEntryLinkRepository.java index 93224f78cd..f608595c3d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseEntryLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseEntryLinkRepository.java @@ -40,7 +40,7 @@ import org.springframework.stereotype.Component; * * @author Andrea Bollini (andrea.bollini at 4science.it) */ -@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME + "." + BrowseIndexRest.ENTRIES) +@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME + "." + BrowseIndexRest.LINK_ENTRIES) public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { @@ -127,7 +127,8 @@ public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository @Override public boolean isEmbeddableRelation(Object data, String name) { BrowseIndexRest bir = (BrowseIndexRest) data; - if (bir.isMetadataBrowse() && "entries".equals(name)) { + if (bir.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_VALUE_LIST) && + name.equals(BrowseIndexRest.LINK_ENTRIES)) { return true; } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseIndexRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseIndexRestRepository.java index 01277ff29b..c87cbc6c03 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseIndexRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseIndexRestRepository.java @@ -13,7 +13,10 @@ import java.util.List; import org.dspace.app.rest.model.BrowseIndexRest; import org.dspace.browse.BrowseException; import org.dspace.browse.BrowseIndex; +import org.dspace.content.authority.DSpaceControlledVocabularyIndex; +import org.dspace.content.authority.service.ChoiceAuthorityService; import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.access.prepost.PreAuthorize; @@ -27,20 +30,39 @@ import org.springframework.stereotype.Component; @Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME) public class BrowseIndexRestRepository extends DSpaceRestRepository { + @Autowired + private ChoiceAuthorityService choiceAuthorityService; + @Override @PreAuthorize("permitAll()") public BrowseIndexRest findOne(Context context, String name) { - BrowseIndexRest bi = null; + BrowseIndexRest bi = createFromMatchingBrowseIndex(name); + if (bi == null) { + bi = createFromMatchingVocabulary(name); + } + + return bi; + } + + private BrowseIndexRest createFromMatchingVocabulary(String name) { + DSpaceControlledVocabularyIndex vocabularyIndex = choiceAuthorityService.getVocabularyIndex(name); + if (vocabularyIndex != null) { + return converter.toRest(vocabularyIndex, utils.obtainProjection()); + } + return null; + } + + private BrowseIndexRest createFromMatchingBrowseIndex(String name) { BrowseIndex bix; try { - bix = BrowseIndex.getBrowseIndex(name); + bix = BrowseIndex.getBrowseIndex(name); } catch (BrowseException e) { throw new RuntimeException(e.getMessage(), e); } if (bix != null) { - bi = converter.toRest(bix, utils.obtainProjection()); + return converter.toRest(bix, utils.obtainProjection()); } - return bi; + return null; } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseItemLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseItemLinkRepository.java index 74aa9f38bf..baa79bc80a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseItemLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseItemLinkRepository.java @@ -42,7 +42,7 @@ import org.springframework.stereotype.Component; * * @author Andrea Bollini (andrea.bollini at 4science.it) */ -@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME + "." + BrowseIndexRest.ITEMS) +@Component(BrowseIndexRest.CATEGORY + "." + BrowseIndexRest.NAME + "." + BrowseIndexRest.LINK_ITEMS) public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { @@ -155,7 +155,8 @@ public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository @Override public boolean isEmbeddableRelation(Object data, String name) { BrowseIndexRest bir = (BrowseIndexRest) data; - if (!bir.isMetadataBrowse() && "items".equals(name)) { + if (bir.getBrowseType().equals(BrowseIndexRest.BROWSE_TYPE_FLAT) && + name.equals(BrowseIndexRest.LINK_ITEMS)) { return true; } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyRestRepository.java index dcdf71186b..fcc37d1316 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/VocabularyRestRepository.java @@ -53,7 +53,7 @@ public class VocabularyRestRepository extends DSpaceRestRepository Date: Fri, 5 May 2023 12:35:40 +0200 Subject: [PATCH 20/63] 94299: Remove bitstreams in bulk via patch --- .../src/main/resources/Messages.properties | 2 + .../rest/BitstreamCategoryRestController.java | 63 ++++++++++++++++++ .../DSpaceApiExceptionControllerAdvice.java | 1 + .../RESTBitstreamNotFoundException.java | 51 +++++++++++++++ .../repository/BitstreamRestRepository.java | 19 ++++++ .../operation/BitstreamRemoveOperation.java | 65 +++++++++++++++++++ 6 files changed, 201 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RESTBitstreamNotFoundException.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java diff --git a/dspace-api/src/main/resources/Messages.properties b/dspace-api/src/main/resources/Messages.properties index b537819c06..78e2774013 100644 --- a/dspace-api/src/main/resources/Messages.properties +++ b/dspace-api/src/main/resources/Messages.properties @@ -120,3 +120,5 @@ org.dspace.app.rest.exception.RESTEmptyWorkflowGroupException.message = Refused org.dspace.app.rest.exception.EPersonNameNotProvidedException.message = The eperson.firstname and eperson.lastname values need to be filled in org.dspace.app.rest.exception.GroupNameNotProvidedException.message = Cannot create group, no group name is provided org.dspace.app.rest.exception.GroupHasPendingWorkflowTasksException.message = Cannot delete group, the associated workflow role still has pending tasks +org.dspace.app.rest.exception.RESTBitstreamNotFoundException.message = Bitstream with uuid {0} could not be found in \ + the repository diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java new file mode 100644 index 0000000000..13929e5a9a --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java @@ -0,0 +1,63 @@ +/** + * 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; + +import static org.dspace.app.rest.utils.ContextUtil.obtainContext; + +import java.sql.SQLException; +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.databind.JsonNode; +import org.dspace.app.rest.model.BitstreamRest; +import org.dspace.app.rest.repository.BitstreamRestRepository; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST controller for handling bulk updates to Bitstream resources. + *

+ * This controller is responsible for handling requests to the bitstream category, which allows for updating + * multiple bitstream resources in a single operation. + *

+ * + * @author Jens Vannerum (jens.vannerum@atmire.com) + */ +@RestController +@RequestMapping("/api/" + BitstreamRest.CATEGORY + "/" + BitstreamRest.PLURAL_NAME) +public class BitstreamCategoryRestController { + @Autowired + BitstreamRestRepository bitstreamRestRepository; + + /** + * Handles PATCH requests to the bitstream category for bulk updates of bitstream resources. + * + * @param request the HTTP request object. + * @param jsonNode the JSON representation of the bulk update operation, containing the updates to be applied. + * @return a ResponseEntity representing the HTTP response to be sent back to the client, in this case, a + * HTTP 204 No Content response since currently only a delete operation is supported. + * @throws SQLException if an error occurs while accessing the database. + * @throws AuthorizeException if the user is not authorized to perform the requested operation. + */ + @PreAuthorize("hasAuthority('ADMIN')") + @RequestMapping(method = RequestMethod.PATCH) + public ResponseEntity> patch(HttpServletRequest request, + @RequestBody(required = true) JsonNode jsonNode) + throws SQLException, AuthorizeException { + Context context = obtainContext(request); + bitstreamRestRepository.patchBitstreamsInBulk(context, jsonNode); + return ResponseEntity.noContent().build(); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index 6ded477813..3f55536666 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -163,6 +163,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH EPersonNameNotProvidedException.class, GroupNameNotProvidedException.class, GroupHasPendingWorkflowTasksException.class, + RESTBitstreamNotFoundException.class }) protected void handleCustomUnprocessableEntityException(HttpServletRequest request, HttpServletResponse response, TranslatableException ex) throws IOException { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RESTBitstreamNotFoundException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RESTBitstreamNotFoundException.java new file mode 100644 index 0000000000..a0b48e3c0d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RESTBitstreamNotFoundException.java @@ -0,0 +1,51 @@ +/** + * 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.exception; + +import java.text.MessageFormat; + +import org.dspace.core.Context; +import org.dspace.core.I18nUtil; + +/** + *

Extend {@link UnprocessableEntityException} to provide a specific error message + * in the REST response. The error message is added to the response in + * {@link DSpaceApiExceptionControllerAdvice#handleCustomUnprocessableEntityException}, + * hence it should not contain sensitive or security-compromising info.

+ * + * @author Jens Vannerum (jens.vannerum@atmire.com) + */ +public class RESTBitstreamNotFoundException extends UnprocessableEntityException implements TranslatableException { + + public static String uuid; + + /** + * @param formatStr string with placeholders, ideally obtained using {@link I18nUtil} + * @return message with bitstream id substituted + */ + private static String formatMessage(String formatStr) { + MessageFormat fmt = new MessageFormat(formatStr); + return fmt.format(new String[]{uuid}); + } + + public static final String MESSAGE_KEY = "org.dspace.app.rest.exception.RESTBitstreamNotFoundException.message"; + + public RESTBitstreamNotFoundException(String uuid) { + super(formatMessage(I18nUtil.getMessage(MESSAGE_KEY))); + RESTBitstreamNotFoundException.uuid = uuid; + } + + public String getMessageKey() { + return MESSAGE_KEY; + } + + public String getLocalizedMessage(Context context) { + return formatMessage(I18nUtil.getMessage(MESSAGE_KEY, context)); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java index 3696b38668..8ef06ecbad 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java @@ -17,9 +17,12 @@ import java.util.List; import java.util.UUID; import javax.servlet.http.HttpServletRequest; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.converter.JsonPatchConverter; import org.dspace.app.rest.exception.DSpaceBadRequestException; import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; import org.dspace.app.rest.exception.UnprocessableEntityException; @@ -292,4 +295,20 @@ public class BitstreamRestRepository extends DSpaceObjectRestRepository + * curl -X PATCH http://${dspace.server.url}/api/core/bitstreams -H "Content-Type: application/json" + * -d '[ + * {"op": "remove", "path": "/bitstreams/${bitstream1UUID}"}, + * {"op": "remove", "path": "/bitstreams/${bitstream2UUID}"}, + * {"op": "remove", "path": "/bitstreams/${bitstream3UUID}"} + * ]' + * + * + * @author Jens Vannerum (jens.vannerum@atmire.com) + */ +@Component +public class BitstreamRemoveOperation extends PatchOperation { + @Autowired + BitstreamService bitstreamService; + private static final String OPERATION_PATH_BITSTREAM_REMOVE = "/bitstreams/"; + + @Override + public Bitstream perform(Context context, Bitstream resource, Operation operation) throws SQLException { + String bitstreamIDtoDelete = operation.getPath().replace(OPERATION_PATH_BITSTREAM_REMOVE, ""); + Bitstream bitstreamToDelete = bitstreamService.find(context, UUID.fromString(bitstreamIDtoDelete)); + if (bitstreamToDelete == null) { + throw new RESTBitstreamNotFoundException(bitstreamIDtoDelete); + } + + try { + bitstreamService.delete(context, bitstreamToDelete); + bitstreamService.update(context, bitstreamToDelete); + } catch (AuthorizeException | IOException e) { + throw new RuntimeException(e.getMessage(), e); + } + return null; + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return objectToMatch == null && operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + operation.getPath().trim().startsWith(OPERATION_PATH_BITSTREAM_REMOVE); + } +} From 09b56c2d99b770d376a3e49cb7f01b3ca0a4f5eb Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Fri, 5 May 2023 13:05:34 +0200 Subject: [PATCH 21/63] 94299: Configurable limit on amount of patch operations --- .../app/rest/repository/BitstreamRestRepository.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java index 8ef06ecbad..586525bbd2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java @@ -43,6 +43,7 @@ import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; import org.dspace.core.Context; import org.dspace.handle.service.HandleService; +import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -77,6 +78,9 @@ public class BitstreamRestRepository extends DSpaceObjectRestRepository operationsLimit) { + throw new DSpaceBadRequestException("The number of operations in the patch is over the limit of " + + operationsLimit); + } resourcePatch.patch(obtainContext(), null, patch.getOperations()); context.commit(); } From 80706592aae50681dde850391770e6fe09ee6eca Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Fri, 5 May 2023 13:07:47 +0200 Subject: [PATCH 22/63] Revert "94299 Multiple Bitstream deletion endpoint" This reverts commit 51d8874a --- .../app/rest/RestResourceController.java | 33 - .../repository/BitstreamRestRepository.java | 44 - .../rest/repository/DSpaceRestRepository.java | 18 - .../app/rest/BitstreamRestRepositoryIT.java | 955 ------------------ 4 files changed, 1050 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java index 24468660f0..b82b483075 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -7,7 +7,6 @@ */ package org.dspace.app.rest; -import static org.dspace.app.rest.utils.ContextUtil.obtainContext; import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_DIGIT; import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_HEX32; import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG; @@ -56,8 +55,6 @@ import org.dspace.app.rest.repository.LinkRestRepository; import org.dspace.app.rest.utils.RestRepositoryUtils; import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.AuthorizeException; -import org.dspace.content.DSpaceObject; -import org.dspace.core.Context; import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; @@ -1053,13 +1050,6 @@ public class RestResourceController implements InitializingBean { return deleteInternal(apiCategory, model, uuid); } - @RequestMapping(method = RequestMethod.DELETE, consumes = {"text/uri-list"}) - public ResponseEntity> delete(HttpServletRequest request, @PathVariable String apiCategory, - @PathVariable String model) - throws HttpRequestMethodNotSupportedException { - return deleteUriListInternal(request, apiCategory, model); - } - /** * Internal method to delete resource. * @@ -1077,29 +1067,6 @@ public class RestResourceController implements InitializingBean { return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } - public ResponseEntity> deleteUriListInternal( - HttpServletRequest request, - String apiCategory, - String model) - throws HttpRequestMethodNotSupportedException { - checkModelPluralForm(apiCategory, model); - DSpaceRestRepository repository = utils.getResourceRepository(apiCategory, model); - Context context = obtainContext(request); - List dsoStringList = utils.getStringListFromRequest(request); - List dsoList = utils.constructDSpaceObjectList(context, dsoStringList); - if (dsoStringList.size() != dsoList.size()) { - throw new ResourceNotFoundException("One or more bitstreams could not be found."); - } - try { - repository.delete(dsoList); - } catch (ClassCastException e) { - log.error("Something went wrong whilst creating the object for apiCategory: " + apiCategory + - " and model: " + model, e); - return ControllerUtils.toEmptyResponse(HttpStatus.INTERNAL_SERVER_ERROR); - } - return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); - } - /** * Execute a PUT request for an entity with id of type UUID; * diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java index 586525bbd2..454b6f8453 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java @@ -10,8 +10,6 @@ package org.dspace.app.rest.repository; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -156,48 +154,6 @@ public class BitstreamRestRepository extends DSpaceObjectRestRepository dsoList) - throws SQLException, AuthorizeException { - // check if list is empty - if (dsoList.isEmpty()) { - throw new ResourceNotFoundException("No bitstreams given."); - } - // check if every DSO is a Bitstream - if (dsoList.stream().anyMatch(dso -> !(dso instanceof Bitstream))) { - throw new UnprocessableEntityException("Not all given items are bitstreams."); - } - // check that they're all part of the same Item - List parents = new ArrayList<>(); - for (DSpaceObject dso : dsoList) { - Bitstream bit = bs.find(context, dso.getID()); - DSpaceObject bitstreamParent = bs.getParentObject(context, bit); - if (bit == null) { - throw new ResourceNotFoundException("The bitstream with uuid " + dso.getID() + " could not be found"); - } - // we have to check if the bitstream has already been deleted - if (bit.isDeleted()) { - throw new UnprocessableEntityException("The bitstream with uuid " + bit.getID() - + " was already deleted"); - } else { - parents.add(bitstreamParent); - } - } - if (parents.stream().distinct().count() > 1) { - throw new UnprocessableEntityException("Not all given items are part of the same Item."); - } - // delete all Bitstreams - Iterator iterator = dsoList.iterator(); - while (iterator.hasNext()) { - Bitstream bit = (Bitstream) iterator.next(); - try { - bs.delete(context, bit); - } catch (SQLException | IOException e) { - throw new RuntimeException(e.getMessage(), e); - } - } - } - /** * Find the bitstream for the provided handle and sequence or filename. * When a bitstream can be found with the sequence ID it will be returned if the user has "METADATA_READ" access. diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java index 219b7c4123..01f127eca5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java @@ -26,7 +26,6 @@ import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.patch.Patch; import org.dspace.authorize.AuthorizeException; -import org.dspace.content.DSpaceObject; import org.dspace.content.service.MetadataFieldService; import org.dspace.core.Context; import org.springframework.beans.factory.BeanNameAware; @@ -257,23 +256,6 @@ public abstract class DSpaceRestRepository dsoList) { - Context context = obtainContext(); - try { - getThisRepository().deleteList(context, dsoList); - context.commit(); - } catch (AuthorizeException e) { - throw new RESTAuthorizationException(e); - } catch (SQLException ex) { - throw new RuntimeException(ex.getMessage(), ex); - } - } - - protected void deleteList(Context context, List list) - throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException { - throw new RepositoryMethodNotImplementedException("No implementation found; Method not allowed!", ""); - } - @Override /** * This method cannot be implemented we required all the find method to be paginated diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index 391d9e4193..f9c1e469fc 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -13,7 +13,6 @@ import static org.dspace.core.Constants.WRITE; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; -import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST; 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.result.MockMvcResultMatchers.content; @@ -1202,960 +1201,6 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest .andExpect(status().isNotFound()); } - @Test - public void deleteListOneBitstream() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - String bitstreamContent = "ThisIsSomeDummyText"; - //Add a bitstream to an item - Bitstream bitstream = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { - bitstream = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream") - .withDescription("Description") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream.getID())) - .andExpect(status().is(204)); - - // Verify 404 after delete - getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID())) - .andExpect(status().isNotFound()); - } - - @Test - public void deleteListOneOfMultipleBitstreams() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 3 bitstreams to the item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent3 = "ThisIsSomeDummyText3"; - Bitstream bitstream3 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { - bitstream3 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream3") - .withDescription("Description3") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete bitstream1 - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().is(204)); - - // Verify 404 after delete for bitstream1 - getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isNotFound()); - - // check that bitstream2 still exists - getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$", HalMatcher.matchNoEmbeds())); - - // check that bitstream3 still exists - getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$", HalMatcher.matchNoEmbeds())) - ; - } - - @Test - public void deleteListAllBitstreams() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 3 bitstreams to the item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent3 = "ThisIsSomeDummyText3"; - Bitstream bitstream3 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { - bitstream3 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream3") - .withDescription("Description3") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete all bitstreams - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().is(204)); - - // Verify 404 after delete for bitstream1 - getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isNotFound()); - - // Verify 404 after delete for bitstream2 - getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isNotFound()); - - // Verify 404 after delete for bitstream3 - getClient(token).perform(get("/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isNotFound()); - } - - @Test - public void deleteListForbidden() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 3 bitstreams to the item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent3 = "ThisIsSomeDummyText3"; - Bitstream bitstream3 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { - bitstream3 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream3") - .withDescription("Description3") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(eperson.getEmail(), password); - - // Delete using an unauthorized user - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isForbidden()); - - // Verify the bitstreams are still here - getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isOk()); - - getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isOk()); - - getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isOk()); - } - - @Test - public void deleteListUnauthorized() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 3 bitstreams to the item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent3 = "ThisIsSomeDummyText3"; - Bitstream bitstream3 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { - bitstream3 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream3") - .withDescription("Description3") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - // Delete as anonymous - getClient().perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isUnauthorized()); - - // Verify the bitstreams are still here - getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isOk()); - - getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isOk()); - - getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isOk()); - } - - @Test - public void deleteListEmpty() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 3 bitstreams to the item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent3 = "ThisIsSomeDummyText3"; - Bitstream bitstream3 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { - bitstream3 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream3") - .withDescription("Description3") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete with empty list throws 404 - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("")) - .andExpect(status().isNotFound()); - - // Verify the bitstreams are still here - getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isOk()); - - getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isOk()); - - getClient(token).perform(get("/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isOk()); - } - - @Test - public void deleteListNotBitstream() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 3 bitstreams to the item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent3 = "ThisIsSomeDummyText3"; - Bitstream bitstream3 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { - bitstream3 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream3") - .withDescription("Description3") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete with list containing non-Bitstream throws 422 - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID() - + " \n http://localhost:8080/server/api/core/items/" + publicItem1.getID())) - .andExpect(status().is(422)); - - // Verify the bitstreams are still here - getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isOk()); - - getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isOk()); - - getClient(token).perform(get("/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isOk()); - } - - @Test - public void deleteListDifferentItems() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. Two public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - Item publicItem2 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 1 bitstream to each item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem2, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete with list containing Bitstreams from different items throws 422 - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().is(422)); - - // Verify the bitstreams are still here - getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isOk()); - - getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isOk()); - - } - - @Test - public void deleteListLogo() throws Exception { - // We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - // ** GIVEN ** - // 1. A community with a logo - parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").withLogo("logo_community") - .build(); - - // 2. A collection with a logo - Collection col = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection") - .withLogo("logo_collection").build(); - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // trying to DELETE parentCommunity logo and collection logo should work - // we have to delete them separately otherwise it will throw 422 as they belong to different items - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + parentCommunity.getLogo().getID())) - .andExpect(status().is(204)); - - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + col.getLogo().getID())) - .andExpect(status().is(204)); - - // Verify 404 after delete for parentCommunity logo - getClient(token).perform(get("/api/core/bitstreams/" + parentCommunity.getLogo().getID())) - .andExpect(status().isNotFound()); - - // Verify 404 after delete for collection logo - getClient(token).perform(get("/api/core/bitstreams/" + col.getLogo().getID())) - .andExpect(status().isNotFound()); - } - - @Test - public void deleteListMissing() throws Exception { - String token = getAuthToken(admin.getEmail(), password); - - // Delete - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb")) - .andExpect(status().isNotFound()); - - // Verify 404 after failed delete - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb")) - .andExpect(status().isNotFound()); - } - - @Test - public void deleteListOneMissing() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 3 bitstreams to the item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent3 = "ThisIsSomeDummyText3"; - Bitstream bitstream3 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { - bitstream3 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream3") - .withDescription("Description3") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete all bitstreams and a missing bitstream returns 404 - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb")) - .andExpect(status().isNotFound()); - - // Verify the bitstreams are still here - getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isOk()); - - getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isOk()); - - getClient(token).perform(get("/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().isOk()); - } - - @Test - public void deleteListOneMissingDifferentItems() throws Exception { - - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. Two public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - Item publicItem2 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 1 bitstream to each item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem2, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete all bitstreams and a missing bitstream returns 404 - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb")) - .andExpect(status().isNotFound()); - - // Verify the bitstreams are still here - getClient(token).perform(get("/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().isOk()); - - getClient(token).perform(get("/api/core/bitstreams/" + bitstream2.getID())) - .andExpect(status().isOk()); - - } - - @Test - public void deleteListDeleted() throws Exception { - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - String bitstreamContent = "ThisIsSomeDummyText"; - //Add a bitstream to an item - Bitstream bitstream = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { - bitstream = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream") - .withDescription("Description") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream.getID())) - .andExpect(status().is(204)); - - // Verify 404 when trying to delete a non-existing, already deleted, bitstream - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream.getID())) - .andExpect(status().is(422)); - } - - @Test - public void deleteListOneDeleted() throws Exception { - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - //** GIVEN ** - //1. A community-collection structure with one parent community with sub-community and one collection. - 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(); - - //2. One public items that is readable by Anonymous - Item publicItem1 = ItemBuilder.createItem(context, col1) - .withTitle("Test") - .withIssueDate("2010-10-17") - .withAuthor("Smith, Donald") - .withSubject("ExtraEntry") - .build(); - - // Add 3 bitstreams to the item - String bitstreamContent1 = "ThisIsSomeDummyText1"; - Bitstream bitstream1 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { - bitstream1 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream1") - .withDescription("Description1") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent2 = "ThisIsSomeDummyText2"; - Bitstream bitstream2 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { - bitstream2 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream2") - .withDescription("Description2") - .withMimeType("text/plain") - .build(); - } - - String bitstreamContent3 = "ThisIsSomeDummyText3"; - Bitstream bitstream3 = null; - try (InputStream is = IOUtils.toInputStream(bitstreamContent3, CharEncoding.UTF_8)) { - bitstream3 = BitstreamBuilder. - createBitstream(context, publicItem1, is) - .withName("Bitstream3") - .withDescription("Description3") - .withMimeType("text/plain") - .build(); - } - - context.restoreAuthSystemState(); - - String token = getAuthToken(admin.getEmail(), password); - - // Delete bitstream1 - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID())) - .andExpect(status().is(204)); - - // Verify 404 when trying to delete a non-existing, already deleted, bitstream - getClient(token).perform(delete("/api/core/bitstreams") - .contentType(TEXT_URI_LIST) - .content("http://localhost:8080/server/api/core/bitstreams/" + bitstream1.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream2.getID() - + " \n http://localhost:8080/server/api/core/bitstreams/" + bitstream3.getID())) - .andExpect(status().is(422)); - } - @Test public void patchBitstreamMetadataAuthorized() throws Exception { runPatchMetadataTests(admin, 200); From 648b27befbe992515319a79d78d01b8327c338d4 Mon Sep 17 00:00:00 2001 From: Nona Luypaert Date: Fri, 5 May 2023 14:34:53 +0200 Subject: [PATCH 23/63] 101549: Make BrowseIndexRestRepository#findAll also return hierarchicalBrowses --- .../java/org/dspace/browse/BrowseIndex.java | 8 ++-- .../DSpaceControlledVocabularyIndex.java | 4 +- .../rest/converter/BrowseIndexConverter.java | 24 ++++++++--- .../HierarchicalBrowseConverter.java | 42 ------------------- .../repository/BrowseIndexRestRepository.java | 6 ++- 5 files changed, 32 insertions(+), 52 deletions(-) delete mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/HierarchicalBrowseConverter.java diff --git a/dspace-api/src/main/java/org/dspace/browse/BrowseIndex.java b/dspace-api/src/main/java/org/dspace/browse/BrowseIndex.java index 8d065c21ce..6c38c8dd66 100644 --- a/dspace-api/src/main/java/org/dspace/browse/BrowseIndex.java +++ b/dspace-api/src/main/java/org/dspace/browse/BrowseIndex.java @@ -22,11 +22,13 @@ import org.dspace.sort.SortOption; * This class holds all the information about a specifically configured * BrowseIndex. It is responsible for parsing the configuration, understanding * about what sort options are available, and what the names of the database - * tables that hold all the information are actually called. + * tables that hold all the information are actually called. Hierarchical browse + * indexes also contain information about the vocabulary they're using, see: + * {@link org.dspace.content.authority.DSpaceControlledVocabularyIndex} * * @author Richard Jones */ -public final class BrowseIndex { +public class BrowseIndex { /** the configuration number, as specified in the config */ /** * used for single metadata browse tables for generating the table name @@ -102,7 +104,7 @@ public final class BrowseIndex { * * @param baseName The base of the table name */ - private BrowseIndex(String baseName) { + protected BrowseIndex(String baseName) { try { number = -1; tableBaseName = baseName; diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabularyIndex.java b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabularyIndex.java index 6f350fc71e..bf8194dbd5 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabularyIndex.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DSpaceControlledVocabularyIndex.java @@ -9,6 +9,7 @@ package org.dspace.content.authority; import java.util.Set; +import org.dspace.browse.BrowseIndex; import org.dspace.discovery.configuration.DiscoverySearchFilterFacet; /** @@ -18,7 +19,7 @@ import org.dspace.discovery.configuration.DiscoverySearchFilterFacet; * * @author Marie Verdonck (Atmire) on 04/05/2023 */ -public class DSpaceControlledVocabularyIndex { +public class DSpaceControlledVocabularyIndex extends BrowseIndex { protected DSpaceControlledVocabulary vocabulary; protected Set metadataFields; @@ -26,6 +27,7 @@ public class DSpaceControlledVocabularyIndex { public DSpaceControlledVocabularyIndex(DSpaceControlledVocabulary controlledVocabulary, Set metadataFields, DiscoverySearchFilterFacet facetConfig) { + super(controlledVocabulary.vocabularyName); this.vocabulary = controlledVocabulary; this.metadataFields = metadataFields; this.facetConfig = facetConfig; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/BrowseIndexConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/BrowseIndexConverter.java index 1e2899b396..2595968d4d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/BrowseIndexConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/BrowseIndexConverter.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.converter; import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_FLAT; +import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_HIERARCHICAL; import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_VALUE_LIST; import java.util.ArrayList; @@ -16,6 +17,7 @@ import java.util.List; import org.dspace.app.rest.model.BrowseIndexRest; import org.dspace.app.rest.projection.Projection; import org.dspace.browse.BrowseIndex; +import org.dspace.content.authority.DSpaceControlledVocabularyIndex; import org.dspace.sort.SortException; import org.dspace.sort.SortOption; import org.springframework.stereotype.Component; @@ -33,19 +35,29 @@ public class BrowseIndexConverter implements DSpaceConverter metadataList = new ArrayList(); - if (obj.isMetadataIndex()) { + String id = obj.getName(); + if (obj instanceof DSpaceControlledVocabularyIndex) { + DSpaceControlledVocabularyIndex vocObj = (DSpaceControlledVocabularyIndex) obj; + metadataList = new ArrayList<>(vocObj.getMetadataFields()); + id = vocObj.getVocabulary().getPluginInstanceName(); + bir.setFacetType(vocObj.getFacetConfig().getIndexFieldName()); + bir.setVocabulary(vocObj.getVocabulary().getPluginInstanceName()); + bir.setBrowseType(BROWSE_TYPE_HIERARCHICAL); + } else if (obj.isMetadataIndex()) { for (String s : obj.getMetadata().split(",")) { metadataList.add(s.trim()); } + bir.setDataType(obj.getDataType()); + bir.setOrder(obj.getDefaultOrder()); bir.setBrowseType(BROWSE_TYPE_VALUE_LIST); } else { metadataList.add(obj.getSortOption().getMetadata()); + bir.setDataType(obj.getDataType()); + bir.setOrder(obj.getDefaultOrder()); bir.setBrowseType(BROWSE_TYPE_FLAT); } + bir.setId(id); bir.setMetadataList(metadataList); List sortOptionsList = new ArrayList(); @@ -56,7 +68,9 @@ public class BrowseIndexConverter implements DSpaceConverter { - - @Override - public BrowseIndexRest convert(DSpaceControlledVocabularyIndex obj, Projection projection) { - BrowseIndexRest bir = new BrowseIndexRest(); - bir.setProjection(projection); - bir.setId(obj.getVocabulary().getPluginInstanceName()); - bir.setBrowseType(BrowseIndexRest.BROWSE_TYPE_HIERARCHICAL); - bir.setFacetType(obj.getFacetConfig().getIndexFieldName()); - bir.setVocabulary(obj.getVocabulary().getPluginInstanceName()); - bir.setMetadataList(new ArrayList<>(obj.getMetadataFields())); - return bir; - } - - @Override - public Class getModelClass() { - return DSpaceControlledVocabularyIndex.class; - } -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseIndexRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseIndexRestRepository.java index c87cbc6c03..b166bffda7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseIndexRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BrowseIndexRestRepository.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest.repository; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -68,7 +69,10 @@ public class BrowseIndexRestRepository extends DSpaceRestRepository findAll(Context context, Pageable pageable) { try { - List indexes = Arrays.asList(BrowseIndex.getBrowseIndices()); + List indexes = new ArrayList<>(Arrays.asList(BrowseIndex.getBrowseIndices())); + choiceAuthorityService.getChoiceAuthoritiesNames() + .stream().filter(name -> choiceAuthorityService.getVocabularyIndex(name) != null) + .forEach(name -> indexes.add(choiceAuthorityService.getVocabularyIndex(name))); return converter.toRestPage(indexes, pageable, indexes.size(), utils.obtainProjection()); } catch (BrowseException e) { throw new RuntimeException(e.getMessage(), e); From 999fb46e8dfafe82313d6b19441ec34075a2a4c8 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Fri, 5 May 2023 15:10:12 +0200 Subject: [PATCH 24/63] 94299: Add IT --- .../operation/BitstreamRemoveOperation.java | 2 +- .../app/rest/BitstreamRestRepositoryIT.java | 229 ++++++++++++++++++ 2 files changed, 230 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java index 5d37e04cea..93c495a302 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java @@ -38,7 +38,7 @@ import org.springframework.stereotype.Component; public class BitstreamRemoveOperation extends PatchOperation { @Autowired BitstreamService bitstreamService; - private static final String OPERATION_PATH_BITSTREAM_REMOVE = "/bitstreams/"; + public static final String OPERATION_PATH_BITSTREAM_REMOVE = "/bitstreams/"; @Override public Bitstream perform(Context context, Bitstream resource, Operation operation) throws SQLException { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index f9c1e469fc..3b01b4eac2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -7,22 +7,29 @@ */ package org.dspace.app.rest; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataDoesNotExist; +import static org.dspace.app.rest.repository.patch.operation.BitstreamRemoveOperation.OPERATION_PATH_BITSTREAM_REMOVE; import static org.dspace.core.Constants.WRITE; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; 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; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.InputStream; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.UUID; +import javax.ws.rs.core.MediaType; import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; @@ -30,6 +37,8 @@ import org.dspace.app.rest.matcher.BitstreamFormatMatcher; import org.dspace.app.rest.matcher.BitstreamMatcher; import org.dspace.app.rest.matcher.BundleMatcher; import org.dspace.app.rest.matcher.HalMatcher; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.RemoveOperation; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; import org.dspace.authorize.service.ResourcePolicyService; @@ -52,10 +61,13 @@ import org.dspace.core.Constants; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.hamcrest.Matchers; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.MvcResult; public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest { @@ -2279,6 +2291,223 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest )); } + @Test + public void deleteBitstreamsInBulk() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + Item publicItem1 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 1") + .build(); + Item publicItem2 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 2") + .build(); + String bitstreamContent = "This is an archived bitstream"; + Bitstream bitstream1 = null; + Bitstream bitstream2 = null; + Bitstream bitstream3 = null; + Bitstream bitstream4 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 1") + .withMimeType("text/plain") + .build(); + bitstream2 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 2") + .withMimeType("text/plain") + .build(); + bitstream3 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 3") + .withMimeType("text/plain") + .build(); + bitstream4 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 4") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + // Add three out of four bitstreams to the list of bitstreams to be deleted + List ops = new ArrayList<>(); + RemoveOperation removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream1.getID()); + ops.add(removeOp1); + RemoveOperation removeOp2 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream2.getID()); + ops.add(removeOp2); + RemoveOperation removeOp3 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream3.getID()); + ops.add(removeOp3); + String patchBody = getPatchContent(ops); + String token = getAuthToken(admin.getEmail(), password); + + Assert.assertTrue(bitstreamExists(token, bitstream1, bitstream2, bitstream3, bitstream4)); + + getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isNoContent()); + + // Verify that only the three bitstreams were deleted and the fourth one still exists + Assert.assertTrue(bitstreamNotFound(token, bitstream1, bitstream2, bitstream3)); + Assert.assertTrue(bitstreamExists(token, bitstream4)); + } + + @Test + public void deleteBitstreamsInBulk_invalidUUID() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + Item publicItem1 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 1") + .build(); + Item publicItem2 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 2") + .build(); + + String bitstreamContent = "This is an archived bitstream"; + Bitstream bitstream1 = null; + Bitstream bitstream2 = null; + Bitstream bitstream3 = null; + Bitstream bitstream4 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 1") + .withMimeType("text/plain") + .build(); + bitstream2 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 2") + .withMimeType("text/plain") + .build(); + bitstream3 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 3") + .withMimeType("text/plain") + .build(); + bitstream4 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 4") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + // Add three out of four bitstreams to the list of bitstreams to be deleted + // For the third bitstream, use an invalid UUID + List ops = new ArrayList<>(); + RemoveOperation removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream1.getID()); + ops.add(removeOp1); + RemoveOperation removeOp2 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream2.getID()); + ops.add(removeOp2); + UUID randomUUID = UUID.randomUUID(); + RemoveOperation removeOp3 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + randomUUID); + ops.add(removeOp3); + String patchBody = getPatchContent(ops); + String token = getAuthToken(admin.getEmail(), password); + + Assert.assertTrue(bitstreamExists(token, bitstream1, bitstream2, bitstream3, bitstream4)); + + MvcResult result = getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()) + .andReturn(); + + // Verify our custom error message is returned when an invalid UUID is used + assertEquals("Bitstream with uuid " + randomUUID + " could not be found in the repository", + result.getResponse().getErrorMessage()); + + // Verify that no bitstreams were deleted since the request was invalid + Assert.assertTrue(bitstreamExists(token, bitstream1, bitstream2, bitstream3, bitstream4)); + } + + @Test + public void deleteBitstreamsInBulk_invalidRequestSize() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + Item publicItem1 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 1") + .build(); + Item publicItem2 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 2") + .build(); + + String bitstreamContent = "This is an archived bitstream"; + Bitstream bitstream1 = null; + Bitstream bitstream2 = null; + Bitstream bitstream3 = null; + Bitstream bitstream4 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 1") + .withMimeType("text/plain") + .build(); + bitstream2 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 2") + .withMimeType("text/plain") + .build(); + bitstream3 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 3") + .withMimeType("text/plain") + .build(); + bitstream4 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 4") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + // Add three out of four bitstreams to the list of bitstreams to be deleted + // But set the patch.operations.limit property to 2, so that the request is invalid + List ops = new ArrayList<>(); + RemoveOperation removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream1.getID()); + ops.add(removeOp1); + RemoveOperation removeOp2 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream2.getID()); + ops.add(removeOp2); + RemoveOperation removeOp3 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream3.getID()); + ops.add(removeOp3); + String patchBody = getPatchContent(ops); + String token = getAuthToken(admin.getEmail(), password); + + Assert.assertTrue(bitstreamExists(token, bitstream1, bitstream2, bitstream3, bitstream4)); + DSpaceServicesFactory.getInstance().getConfigurationService().setProperty("patch.operations.limit", 2); + + getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + + // Verify that no bitstreams were deleted since the request was invalid + Assert.assertTrue(bitstreamExists(token, bitstream1, bitstream2, bitstream3, bitstream4)); + } + + public boolean bitstreamExists(String token, Bitstream ...bitstreams) throws Exception { + for (Bitstream bitstream : bitstreams) { + if (getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID())) + .andReturn().getResponse().getStatus() != SC_OK) { + return false; + } + } + return true; + } + + public boolean bitstreamNotFound(String token, Bitstream ...bitstreams) throws Exception { + for (Bitstream bitstream : bitstreams) { + if (getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID())) + .andReturn().getResponse().getStatus() != SC_NOT_FOUND) { + return false; + } + } + return true; + } } From acb700c88774b1aea471b4bf08037a8dcfaa8be5 Mon Sep 17 00:00:00 2001 From: Nona Luypaert Date: Fri, 5 May 2023 15:55:49 +0200 Subject: [PATCH 25/63] 101549: Fix BrowseIndexMatcher and BrowsesResourceControllerIT --- .../app/rest/BrowsesResourceControllerIT.java | 26 +++++++++++--- .../app/rest/matcher/BrowseIndexMatcher.java | 34 ++++++++++++++++--- 2 files changed, 50 insertions(+), 10 deletions(-) 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 baf459408d..a5f4af102c 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 @@ -63,22 +63,23 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) - //Our default Discovery config has 4 browse indexes so we expect this to be reflected in the page + //Our default Discovery config has 5 browse indexes, so we expect this to be reflected in the page // object .andExpect(jsonPath("$.page.size", is(20))) - .andExpect(jsonPath("$.page.totalElements", is(4))) + .andExpect(jsonPath("$.page.totalElements", is(5))) .andExpect(jsonPath("$.page.totalPages", is(1))) .andExpect(jsonPath("$.page.number", is(0))) - //The array of browse index should have a size 4 - .andExpect(jsonPath("$._embedded.browses", hasSize(4))) + //The array of browse index should have a size 5 + .andExpect(jsonPath("$._embedded.browses", hasSize(5))) //Check that all (and only) the default browse indexes are present .andExpect(jsonPath("$._embedded.browses", containsInAnyOrder( BrowseIndexMatcher.dateIssuedBrowseIndex("asc"), BrowseIndexMatcher.contributorBrowseIndex("asc"), BrowseIndexMatcher.titleBrowseIndex("asc"), - BrowseIndexMatcher.subjectBrowseIndex("asc") + BrowseIndexMatcher.subjectBrowseIndex("asc"), + BrowseIndexMatcher.hierarchicalBrowseIndex("srsc") ))) ; } @@ -125,6 +126,21 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe ; } + @Test + public void findBrowseByVocabulary() throws Exception { + //Use srsc as this vocabulary is included by default + //When we call the root endpoint + getClient().perform(get("/api/discover/browses/srsc")) + //The status has to be 200 OK + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + + //Check that the JSON root matches the expected browse index + .andExpect(jsonPath("$", BrowseIndexMatcher.hierarchicalBrowseIndex("srsc"))) + ; + } + @Test public void findBrowseBySubject() throws Exception { //When we call the root endpoint diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BrowseIndexMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BrowseIndexMatcher.java index 82d611facf..80f27b6bbb 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BrowseIndexMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BrowseIndexMatcher.java @@ -8,6 +8,9 @@ package org.dspace.app.rest.matcher; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_FLAT; +import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_HIERARCHICAL; +import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_VALUE_LIST; import static org.dspace.app.rest.test.AbstractControllerIntegrationTest.REST_SERVER_URL; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.contains; @@ -16,7 +19,6 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.text.IsEqualIgnoringCase.equalToIgnoringCase; import org.hamcrest.Matcher; -import org.hamcrest.Matchers; /** * Utility class to construct a Matcher for a browse index @@ -31,7 +33,8 @@ public class BrowseIndexMatcher { public static Matcher subjectBrowseIndex(final String order) { return allOf( hasJsonPath("$.metadata", contains("dc.subject.*")), - hasJsonPath("$.metadataBrowse", Matchers.is(true)), + hasJsonPath("$.browseType", equalToIgnoringCase(BROWSE_TYPE_VALUE_LIST)), + hasJsonPath("$.type", equalToIgnoringCase("browse")), hasJsonPath("$.dataType", equalToIgnoringCase("text")), hasJsonPath("$.order", equalToIgnoringCase(order)), hasJsonPath("$.sortOptions[*].name", containsInAnyOrder("title", "dateissued", "dateaccessioned")), @@ -44,7 +47,8 @@ public class BrowseIndexMatcher { public static Matcher titleBrowseIndex(final String order) { return allOf( hasJsonPath("$.metadata", contains("dc.title")), - hasJsonPath("$.metadataBrowse", Matchers.is(false)), + hasJsonPath("$.browseType", equalToIgnoringCase(BROWSE_TYPE_FLAT)), + hasJsonPath("$.type", equalToIgnoringCase("browse")), hasJsonPath("$.dataType", equalToIgnoringCase("title")), hasJsonPath("$.order", equalToIgnoringCase(order)), hasJsonPath("$.sortOptions[*].name", containsInAnyOrder("title", "dateissued", "dateaccessioned")), @@ -56,7 +60,8 @@ public class BrowseIndexMatcher { public static Matcher contributorBrowseIndex(final String order) { return allOf( hasJsonPath("$.metadata", contains("dc.contributor.*", "dc.creator")), - hasJsonPath("$.metadataBrowse", Matchers.is(true)), + hasJsonPath("$.browseType", equalToIgnoringCase(BROWSE_TYPE_VALUE_LIST)), + hasJsonPath("$.type", equalToIgnoringCase("browse")), hasJsonPath("$.dataType", equalToIgnoringCase("text")), hasJsonPath("$.order", equalToIgnoringCase(order)), hasJsonPath("$.sortOptions[*].name", containsInAnyOrder("title", "dateissued", "dateaccessioned")), @@ -69,7 +74,8 @@ public class BrowseIndexMatcher { public static Matcher dateIssuedBrowseIndex(final String order) { return allOf( hasJsonPath("$.metadata", contains("dc.date.issued")), - hasJsonPath("$.metadataBrowse", Matchers.is(false)), + hasJsonPath("$.browseType", equalToIgnoringCase(BROWSE_TYPE_FLAT)), + hasJsonPath("$.type", equalToIgnoringCase("browse")), hasJsonPath("$.dataType", equalToIgnoringCase("date")), hasJsonPath("$.order", equalToIgnoringCase(order)), hasJsonPath("$.sortOptions[*].name", containsInAnyOrder("title", "dateissued", "dateaccessioned")), @@ -77,4 +83,22 @@ public class BrowseIndexMatcher { hasJsonPath("$._links.items.href", is(REST_SERVER_URL + "discover/browses/dateissued/items")) ); } + + public static Matcher hierarchicalBrowseIndex(final String vocabulary) { + return allOf( + hasJsonPath("$.metadata", contains("dc.subject")), + hasJsonPath("$.browseType", equalToIgnoringCase(BROWSE_TYPE_HIERARCHICAL)), + hasJsonPath("$.type", equalToIgnoringCase("browse")), + hasJsonPath("$.facetType", equalToIgnoringCase("subject")), + hasJsonPath("$.vocabulary", equalToIgnoringCase(vocabulary)), + hasJsonPath("$._links.vocabulary.href", + is(REST_SERVER_URL + String.format("submission/vocabularies/%s/", vocabulary))), + hasJsonPath("$._links.items.href", + is(REST_SERVER_URL + String.format("discover/browses/%s/items", vocabulary))), + hasJsonPath("$._links.entries.href", + is(REST_SERVER_URL + String.format("discover/browses/%s/entries", vocabulary))), + hasJsonPath("$._links.self.href", + is(REST_SERVER_URL + String.format("discover/browses/%s", vocabulary))) + ); + } } From ab240d7f0ec07d9454ae925b2a03154c5cb2b80a Mon Sep 17 00:00:00 2001 From: Nona Luypaert Date: Fri, 5 May 2023 17:47:24 +0200 Subject: [PATCH 26/63] 101549: Fix BrowsesResourceControllerIT --- .../org/dspace/app/rest/BrowsesResourceControllerIT.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 301ffeab41..d1791ab872 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 @@ -8,6 +8,7 @@ package org.dspace.app.rest; import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; +import static org.dspace.app.rest.model.BrowseIndexRest.BROWSE_TYPE_VALUE_LIST; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; @@ -2158,7 +2159,7 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe // The browse definition ID should be "author" .andExpect(jsonPath("$.id", is("author"))) // It should be configured as a metadata browse - .andExpect(jsonPath("$.metadataBrowse", is(true))) + .andExpect(jsonPath("$.browseType", is(BROWSE_TYPE_VALUE_LIST))) ; } @@ -2175,7 +2176,7 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe // The browse definition ID should be "author" .andExpect(jsonPath("$.id", is("author"))) // It should be configured as a metadata browse - .andExpect(jsonPath("$.metadataBrowse", is(true))); + .andExpect(jsonPath("$.browseType", is(BROWSE_TYPE_VALUE_LIST))); } @Test From 5088447111dd10f8627fe61d5602e6e331a93ff1 Mon Sep 17 00:00:00 2001 From: Michael W Spalti Date: Sat, 6 May 2023 11:30:51 -0700 Subject: [PATCH 27/63] Updated solr query params. --- .../dspace/app/iiif/service/WordHighlightSolrSearch.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dspace-iiif/src/main/java/org/dspace/app/iiif/service/WordHighlightSolrSearch.java b/dspace-iiif/src/main/java/org/dspace/app/iiif/service/WordHighlightSolrSearch.java index da50f33582..9e6022548d 100644 --- a/dspace-iiif/src/main/java/org/dspace/app/iiif/service/WordHighlightSolrSearch.java +++ b/dspace-iiif/src/main/java/org/dspace/app/iiif/service/WordHighlightSolrSearch.java @@ -118,7 +118,8 @@ public class WordHighlightSolrSearch implements SearchAnnotationService { } /** - * Constructs a solr search URL. + * Constructs a solr search URL. Compatible with solr-ocrhighlighting-0.7.2. + * https://github.com/dbmdz/solr-ocrhighlighting/releases/tag/0.7.2 * * @param query the search terms * @param manifestId the id of the manifest in which to search @@ -132,8 +133,9 @@ public class WordHighlightSolrSearch implements SearchAnnotationService { solrQuery.set("hl.ocr.fl", "ocr_text"); solrQuery.set("hl.ocr.contextBlock", "line"); solrQuery.set("hl.ocr.contextSize", "2"); - solrQuery.set("hl.snippets", "10"); - solrQuery.set("hl.ocr.trackPages", "off"); + solrQuery.set("hl.snippets", "8192"); + solrQuery.set("hl.ocr.maxPassages", "8192"); + solrQuery.set("hl.ocr.trackPages", "on"); solrQuery.set("hl.ocr.limitBlock","page"); solrQuery.set("hl.ocr.absoluteHighlights", "true"); From 7971887b9a8603aa0039f8e5f3595520c0e65c3a Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Fri, 21 Apr 2023 06:54:13 +0200 Subject: [PATCH 28/63] DURACOM-136 allow script execution by user other than admins --- .../ProcessCleanerConfiguration.java | 17 -- .../MetadataDeletionScriptConfiguration.java | 17 -- .../MetadataExportScriptConfiguration.java | 17 -- ...tadataExportSearchScriptConfiguration.java | 6 - .../MetadataImportScriptConfiguration.java | 16 -- .../harvest/HarvestScriptConfiguration.java | 14 - .../ItemExportScriptConfiguration.java | 17 -- .../ItemImportScriptConfiguration.java | 16 -- .../MediaFilterScriptConfiguration.java | 19 -- ...rDatabaseResyncCliScriptConfiguration.java | 6 - .../authorize/AuthorizeServiceImpl.java | 14 + .../authorize/service/AuthorizeService.java | 9 + .../org/dspace/content/dao/ProcessDAO.java | 23 ++ .../content/dao/impl/ProcessDAOImpl.java | 28 ++ .../curate/CurationScriptConfiguration.java | 43 ++- .../IndexDiscoveryScriptConfiguration.java | 17 -- .../OrcidBulkPushScriptConfiguration.java | 17 -- .../dspace/scripts/ProcessServiceImpl.java | 10 + .../org/dspace/scripts/ScriptServiceImpl.java | 2 +- .../configuration/ScriptConfiguration.java | 22 +- .../scripts/service/ProcessService.java | 22 ++ ...iledOpenUrlTrackerScriptConfiguration.java | 17 -- ...nFormsMigrationCliScriptConfiguration.java | 17 -- ...sionFormsMigrationScriptConfiguration.java | 36 ++- ...riptionEmailNotificationConfiguration.java | 16 -- .../builder/AbstractDSpaceObjectBuilder.java | 4 +- .../java/org/dspace/builder/ItemBuilder.java | 4 +- .../org/dspace/builder/ProcessBuilder.java | 3 + ...MockDSpaceRunnableScriptConfiguration.java | 17 -- .../app/rest/ScriptProcessesController.java | 15 +- .../repository/ProcessRestRepository.java | 16 ++ .../rest/repository/ScriptRestRepository.java | 30 +- .../app/rest/ProcessRestRepositoryIT.java | 26 ++ .../app/rest/ScriptRestRepositoryIT.java | 136 ++++++++- ...TypeConversionTestScriptConfiguration.java | 5 - .../org/dspace/curate/CurationScriptIT.java | 267 ++++++++++++++++++ ...MockDSpaceRunnableScriptConfiguration.java | 17 -- 37 files changed, 659 insertions(+), 319 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerConfiguration.java b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerConfiguration.java index 8d189038d9..91dcfb5dfe 100644 --- a/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/administer/ProcessCleanerConfiguration.java @@ -7,33 +7,16 @@ */ package org.dspace.administer; -import java.sql.SQLException; - import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * The {@link ScriptConfiguration} for the {@link ProcessCleaner} script. */ public class ProcessCleanerConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; - @Override - public boolean isAllowedToExecute(Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { if (options == null) { diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataDeletionScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataDeletionScriptConfiguration.java index 9ccd53944a..fb228e7041 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataDeletionScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataDeletionScriptConfiguration.java @@ -7,33 +7,16 @@ */ package org.dspace.app.bulkedit; -import java.sql.SQLException; - import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * The {@link ScriptConfiguration} for the {@link MetadataDeletion} script. */ public class MetadataDeletionScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; - @Override - public boolean isAllowedToExecute(Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { if (options == null) { diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportScriptConfiguration.java index 31556afc8d..aa76c09c0a 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportScriptConfiguration.java @@ -7,22 +7,14 @@ */ package org.dspace.app.bulkedit; -import java.sql.SQLException; - import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * The {@link ScriptConfiguration} for the {@link MetadataExport} script */ public class MetadataExportScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -39,15 +31,6 @@ public class MetadataExportScriptConfiguration extends this.dspaceRunnableClass = dspaceRunnableClass; } - @Override - public boolean isAllowedToExecute(Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { if (options == null) { diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchScriptConfiguration.java index 4e350562bc..4f2a225d3a 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataExportSearchScriptConfiguration.java @@ -9,7 +9,6 @@ package org.dspace.app.bulkedit; import org.apache.commons.cli.Options; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; /** @@ -29,11 +28,6 @@ public class MetadataExportSearchScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -40,15 +33,6 @@ public class MetadataImportScriptConfiguration extends this.dspaceRunnableClass = dspaceRunnableClass; } - @Override - public boolean isAllowedToExecute(Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { if (options == null) { diff --git a/dspace-api/src/main/java/org/dspace/app/harvest/HarvestScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/harvest/HarvestScriptConfiguration.java index 982973e47c..ff83c3ecb2 100644 --- a/dspace-api/src/main/java/org/dspace/app/harvest/HarvestScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/harvest/HarvestScriptConfiguration.java @@ -7,18 +7,11 @@ */ package org.dspace.app.harvest; -import java.sql.SQLException; - import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; public class HarvestScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; private Class dspaceRunnableClass; @@ -32,13 +25,6 @@ public class HarvestScriptConfiguration extends ScriptConfigu this.dspaceRunnableClass = dspaceRunnableClass; } - public boolean isAllowedToExecute(final Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } public Options getOptions() { Options options = new Options(); diff --git a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportScriptConfiguration.java index cf70120d27..b37df5f5ea 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/itemexport/ItemExportScriptConfiguration.java @@ -7,14 +7,9 @@ */ package org.dspace.app.itemexport; -import java.sql.SQLException; - import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * The {@link ScriptConfiguration} for the {@link ItemExport} script @@ -23,9 +18,6 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class ItemExportScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -38,15 +30,6 @@ public class ItemExportScriptConfiguration extends ScriptC this.dspaceRunnableClass = dspaceRunnableClass; } - @Override - public boolean isAllowedToExecute(final Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { Options options = new Options(); diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportScriptConfiguration.java index a3149040c4..fd895e2f44 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportScriptConfiguration.java @@ -8,14 +8,10 @@ package org.dspace.app.itemimport; import java.io.InputStream; -import java.sql.SQLException; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * The {@link ScriptConfiguration} for the {@link ItemImport} script @@ -24,9 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class ItemImportScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -39,15 +32,6 @@ public class ItemImportScriptConfiguration extends ScriptC this.dspaceRunnableClass = dspaceRunnableClass; } - @Override - public boolean isAllowedToExecute(final Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { Options options = new Options(); diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java index 26347c56ee..867e684db8 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java @@ -7,25 +7,16 @@ */ package org.dspace.app.mediafilter; -import java.sql.SQLException; - import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; public class MediaFilterScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; private static final String MEDIA_FILTER_PLUGINS_KEY = "filter.plugins"; - @Override public Class getDspaceRunnableClass() { return dspaceRunnableClass; @@ -36,16 +27,6 @@ public class MediaFilterScriptConfiguration extends this.dspaceRunnableClass = dspaceRunnableClass; } - - @Override - public boolean isAllowedToExecute(final Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { Options options = new Options(); diff --git a/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCliScriptConfiguration.java index b238ccf061..067c76cce8 100644 --- a/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCliScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/solrdatabaseresync/SolrDatabaseResyncCliScriptConfiguration.java @@ -8,7 +8,6 @@ package org.dspace.app.solrdatabaseresync; import org.apache.commons.cli.Options; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; /** @@ -27,11 +26,6 @@ public class SolrDatabaseResyncCliScriptConfiguration extends ScriptConfiguratio this.dspaceRunnableClass = dspaceRunnableClass; } - @Override - public boolean isAllowedToExecute(Context context) { - return true; - } - @Override public Options getOptions() { if (options == null) { 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 34543c078a..bfd933f482 100644 --- a/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authorize/AuthorizeServiceImpl.java @@ -43,6 +43,7 @@ import org.dspace.discovery.SearchService; import org.dspace.discovery.SearchServiceException; import org.dspace.discovery.indexobject.IndexableCollection; import org.dspace.discovery.indexobject.IndexableCommunity; +import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; @@ -809,6 +810,19 @@ public class AuthorizeServiceImpl implements AuthorizeService { return performCheck(context, "search.resourcetype:" + IndexableCollection.TYPE); } + /** + * Checks that the context's current user is an item admin in the site by querying the solr database. + * + * @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, "search.resourcetype:" + IndexableItem.TYPE); + } + /** * Checks that the context's current user is a community or collection admin in the site. * 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 36679f94c6..86ff236168 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 @@ -532,6 +532,15 @@ public interface AuthorizeService { */ boolean isCollectionAdmin(Context context) throws SQLException; + /** + * Checks that the context's current user is an item admin in the site by querying the solr database. + * + * @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 + */ + boolean isItemAdmin(Context context) throws SQLException; + /** * Checks that the context's current user is a community or collection admin in the site. * diff --git a/dspace-api/src/main/java/org/dspace/content/dao/ProcessDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/ProcessDAO.java index 69bac319c6..95ec40c7a5 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/ProcessDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/ProcessDAO.java @@ -14,6 +14,7 @@ import java.util.List; import org.dspace.content.ProcessStatus; import org.dspace.core.Context; import org.dspace.core.GenericDAO; +import org.dspace.eperson.EPerson; import org.dspace.scripts.Process; import org.dspace.scripts.ProcessQueryParameterContainer; @@ -97,4 +98,26 @@ public interface ProcessDAO extends GenericDAO { List findByStatusAndCreationTimeOlderThan(Context context, List statuses, Date date) throws SQLException; + /** + * Returns a list of all Process objects in the database by the given user. + * + * @param context The relevant DSpace context + * @param user The user to search for + * @param limit The limit for the amount of Processes returned + * @param offset The offset for the Processes to be returned + * @return The list of all Process objects in the Database + * @throws SQLException If something goes wrong + */ + List findByUser(Context context, EPerson user, int limit, int offset) throws SQLException; + + /** + * Count all the processes which is related to the given user. + * + * @param context The relevant DSpace context + * @param user The user to search for + * @return The number of results matching the query + * @throws SQLException If something goes wrong + */ + int countByUser(Context context, EPerson user) throws SQLException; + } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/ProcessDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/ProcessDAOImpl.java index 23ce6ce381..d719b5006c 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/ProcessDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/ProcessDAOImpl.java @@ -24,6 +24,7 @@ import org.dspace.content.ProcessStatus; import org.dspace.content.dao.ProcessDAO; import org.dspace.core.AbstractHibernateDAO; import org.dspace.core.Context; +import org.dspace.eperson.EPerson; import org.dspace.scripts.Process; import org.dspace.scripts.ProcessQueryParameterContainer; import org.dspace.scripts.Process_; @@ -168,6 +169,33 @@ public class ProcessDAOImpl extends AbstractHibernateDAO implements Pro return list(context, criteriaQuery, false, Process.class, -1, -1); } + @Override + public List findByUser(Context context, EPerson user, int limit, int offset) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Process.class); + + Root processRoot = criteriaQuery.from(Process.class); + criteriaQuery.select(processRoot); + criteriaQuery.where(criteriaBuilder.equal(processRoot.get(Process_.E_PERSON), user)); + + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.desc(processRoot.get(Process_.PROCESS_ID))); + criteriaQuery.orderBy(orderList); + + return list(context, criteriaQuery, false, Process.class, limit, offset); + } + + @Override + public int countByUser(Context context, EPerson user) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Process.class); + + Root processRoot = criteriaQuery.from(Process.class); + criteriaQuery.select(processRoot); + criteriaQuery.where(criteriaBuilder.equal(processRoot.get(Process_.E_PERSON), user)); + return count(context, criteriaQuery, criteriaBuilder, processRoot); + } + } diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/curate/CurationScriptConfiguration.java index fefb4eb768..2587e6b025 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationScriptConfiguration.java @@ -8,12 +8,15 @@ package org.dspace.curate; import java.sql.SQLException; +import java.util.List; import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.DSpaceObject; import org.dspace.core.Context; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; +import org.dspace.scripts.DSpaceCommandLineParameter; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * The {@link ScriptConfiguration} for the {@link Curation} script @@ -22,9 +25,6 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class CurationScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -38,16 +38,37 @@ public class CurationScriptConfiguration extends ScriptConfi } /** - * Only admin can run Curation script via the scripts and processes endpoints. - * @param context The relevant DSpace context - * @return True if currentUser is admin, otherwise false + * Only repository admins or admins of the target object can run Curation script via the scripts + * and processes endpoints. + * + * @param context The relevant DSpace context + * @param commandLineParameters the parameters that will be used to start the process if known, + * null otherwise + * @return true if the currentUser is allowed to run the script with the specified parameters or + * at least in some case if the parameters are not yet known */ @Override - public boolean isAllowedToExecute(Context context) { + public boolean isAllowedToExecute(Context context, List commandLineParameters) { try { - return authorizeService.isAdmin(context); + if (commandLineParameters == null) { + return authorizeService.isAdmin(context) || authorizeService.isComColAdmin(context) + || authorizeService.isItemAdmin(context); + } else if (commandLineParameters.stream() + .map(DSpaceCommandLineParameter::getName) + .noneMatch("-i"::equals)) { + return authorizeService.isAdmin(context); + } else { + String dspaceObjectID = commandLineParameters.stream() + .filter(parameter -> "-i".equals(parameter.getName())) + .map(DSpaceCommandLineParameter::getValue) + .findFirst() + .get(); + HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); + DSpaceObject dso = handleService.resolveToObject(context, dspaceObjectID); + return authorizeService.isAdmin(context, dso); + } } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); + throw new RuntimeException(e); } } diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexDiscoveryScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/discovery/IndexDiscoveryScriptConfiguration.java index 8bf3cf2aba..8707b733a6 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexDiscoveryScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexDiscoveryScriptConfiguration.java @@ -7,22 +7,14 @@ */ package org.dspace.discovery; -import java.sql.SQLException; - import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * The {@link ScriptConfiguration} for the {@link IndexClient} script */ public class IndexDiscoveryScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -30,15 +22,6 @@ public class IndexDiscoveryScriptConfiguration extends Sc return dspaceRunnableClass; } - @Override - public boolean isAllowedToExecute(Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { if (options == null) { diff --git a/dspace-api/src/main/java/org/dspace/orcid/script/OrcidBulkPushScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/orcid/script/OrcidBulkPushScriptConfiguration.java index 1a657343c0..88a1033eca 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/script/OrcidBulkPushScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/orcid/script/OrcidBulkPushScriptConfiguration.java @@ -7,13 +7,8 @@ */ package org.dspace.orcid.script; -import java.sql.SQLException; - import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * Script configuration for {@link OrcidBulkPush}. @@ -24,20 +19,8 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class OrcidBulkPushScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; - @Override - public boolean isAllowedToExecute(Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Class getDspaceRunnableClass() { return dspaceRunnableClass; diff --git a/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java b/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java index 33fea75add..2e14aeaa36 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/scripts/ProcessServiceImpl.java @@ -129,6 +129,11 @@ public class ProcessServiceImpl implements ProcessService { return processes; } + @Override + public List findByUser(Context context, EPerson eperson, int limit, int offset) throws SQLException { + return processDAO.findByUser(context, eperson, limit, offset); + } + @Override public void start(Context context, Process process) throws SQLException { process.setProcessStatus(ProcessStatus.RUNNING); @@ -311,6 +316,11 @@ public class ProcessServiceImpl implements ProcessService { return this.processDAO.findByStatusAndCreationTimeOlderThan(context, statuses, date); } + @Override + public int countByUser(Context context, EPerson user) throws SQLException { + return processDAO.countByUser(context, user); + } + private String formatLogLine(int processId, String scriptName, String output, ProcessLogLevel processLogLevel) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); StringBuilder sb = new StringBuilder(); diff --git a/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java b/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java index c8a7812a51..abb700cb10 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java @@ -37,7 +37,7 @@ public class ScriptServiceImpl implements ScriptService { @Override public List getScriptConfigurations(Context context) { return serviceManager.getServicesByType(ScriptConfiguration.class).stream().filter( - scriptConfiguration -> scriptConfiguration.isAllowedToExecute(context)) + scriptConfiguration -> scriptConfiguration.isAllowedToExecute(context, null)) .sorted(Comparator.comparing(ScriptConfiguration::getName)) .collect(Collectors.toList()); } diff --git a/dspace-api/src/main/java/org/dspace/scripts/configuration/ScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/scripts/configuration/ScriptConfiguration.java index 4b15c22f44..e22063eb49 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/configuration/ScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/scripts/configuration/ScriptConfiguration.java @@ -7,17 +7,28 @@ */ package org.dspace.scripts.configuration; +import java.sql.SQLException; +import java.util.List; + import org.apache.commons.cli.Options; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.core.Context; +import org.dspace.scripts.DSpaceCommandLineParameter; import org.dspace.scripts.DSpaceRunnable; import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.annotation.Autowired; /** * This class represents an Abstract class that a ScriptConfiguration can inherit to further implement this - * and represent a script's configuration + * and represent a script's configuration. + * By default script are available only to repository administrators script that have a broader audience + * must override the {@link #isAllowedToExecute(Context, List)} method. */ public abstract class ScriptConfiguration implements BeanNameAware { + @Autowired + protected AuthorizeService authorizeService; + /** * The possible options for this script */ @@ -70,6 +81,7 @@ public abstract class ScriptConfiguration implements B * @param dspaceRunnableClass The dspaceRunnableClass to be set on this IndexDiscoveryScriptConfiguration */ public abstract void setDspaceRunnableClass(Class dspaceRunnableClass); + /** * This method will return if the script is allowed to execute in the given context. This is by default set * to the currentUser in the context being an admin, however this can be overwritten by each script individually @@ -77,7 +89,13 @@ public abstract class ScriptConfiguration implements B * @param context The relevant DSpace context * @return A boolean indicating whether the script is allowed to execute or not */ - public abstract boolean isAllowedToExecute(Context context); + public boolean isAllowedToExecute(Context context, List commandLineParameters) { + try { + return authorizeService.isAdmin(context); + } catch (SQLException e) { + throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); + } + } /** * The getter for the options of the Script diff --git a/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java b/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java index ce6a173b0e..c6fc248881 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java +++ b/dspace-api/src/main/java/org/dspace/scripts/service/ProcessService.java @@ -255,4 +255,26 @@ public interface ProcessService { */ List findByStatusAndCreationTimeOlderThan(Context context, List statuses, Date date) throws SQLException; + + /** + * Returns a list of all Process objects in the database by the given user. + * + * @param context The relevant DSpace context + * @param user The user to search for + * @param limit The limit for the amount of Processes returned + * @param offset The offset for the Processes to be returned + * @return The list of all Process objects in the Database + * @throws SQLException If something goes wrong + */ + List findByUser(Context context, EPerson user, int limit, int offset) throws SQLException; + + /** + * Count all the processes which is related to the given user. + * + * @param context The relevant DSpace context + * @param user The user to search for + * @return The number of results matching the query + * @throws SQLException If something goes wrong + */ + int countByUser(Context context, EPerson user) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/statistics/export/RetryFailedOpenUrlTrackerScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/statistics/export/RetryFailedOpenUrlTrackerScriptConfiguration.java index dcae4aa4cb..7d1015c8e2 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/export/RetryFailedOpenUrlTrackerScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/statistics/export/RetryFailedOpenUrlTrackerScriptConfiguration.java @@ -7,13 +7,8 @@ */ package org.dspace.statistics.export; -import java.sql.SQLException; - import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * The {@link ScriptConfiguration} for the {@link RetryFailedOpenUrlTracker} script @@ -21,9 +16,6 @@ import org.springframework.beans.factory.annotation.Autowired; public class RetryFailedOpenUrlTrackerScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -41,15 +33,6 @@ public class RetryFailedOpenUrlTrackerScriptConfiguration extends ScriptConfiguration { - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -38,15 +30,6 @@ public class SubmissionFormsMigrationCliScriptConfiguration + extends ScriptConfiguration { + + private Class dspaceRunnableClass; @Override - public boolean isAllowedToExecute(Context context) { + public Class getDspaceRunnableClass() { + return this.dspaceRunnableClass; + } + + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + @Override + public Options getOptions() { + if (options == null) { + Options options = new Options(); + + options.addOption("f", "input-forms", true, "Path to source input-forms.xml file location"); + options.addOption("s", "item-submission", true, "Path to source item-submission.xml file location"); + options.addOption("h", "help", false, "help"); + + super.options = options; + } + return options; + } + + @Override + public boolean isAllowedToExecute(Context context, List commandLineParameters) { // Script is not allowed to be executed from REST side return false; } diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java index 52685b563d..dd61fab967 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java @@ -8,15 +8,11 @@ package org.dspace.subscriptions; -import java.sql.SQLException; import java.util.Objects; import org.apache.commons.cli.Options; -import org.dspace.authorize.AuthorizeServiceImpl; -import org.dspace.core.Context; import org.dspace.scripts.DSpaceRunnable; import org.dspace.scripts.configuration.ScriptConfiguration; -import org.springframework.beans.factory.annotation.Autowired; /** * Implementation of {@link DSpaceRunnable} to find subscribed objects and send notification mails about them @@ -26,18 +22,6 @@ public class SubscriptionEmailNotificationConfiguration dspaceRunnableClass; - @Autowired - private AuthorizeServiceImpl authorizeService; - - @Override - public boolean isAllowedToExecute(Context context) { - try { - return authorizeService.isAdmin(context); - } catch (SQLException e) { - throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); - } - } - @Override public Options getOptions() { if (Objects.isNull(options)) { diff --git a/dspace-api/src/test/java/org/dspace/builder/AbstractDSpaceObjectBuilder.java b/dspace-api/src/test/java/org/dspace/builder/AbstractDSpaceObjectBuilder.java index ff1083d318..b20515017a 100644 --- a/dspace-api/src/test/java/org/dspace/builder/AbstractDSpaceObjectBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/AbstractDSpaceObjectBuilder.java @@ -162,8 +162,8 @@ public abstract class AbstractDSpaceObjectBuilder return (B) this; } /** - * Support method to grant the {@link Constants#READ} permission over an object only to a specific group. Any other - * READ permissions will be removed + * Support method to grant the {@link Constants#ADMIN} permission over an object only to a specific eperson. + * If another ADMIN policy is in place for an eperson it will be replaced * * @param dso * the DSpaceObject on which grant the permission 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 70dea309f2..3e5ab0f38f 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java @@ -353,9 +353,9 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder { } /** - * Create an admin group for the collection with the specified members + * Assign the admin permission to the specified eperson * - * @param ePerson epersons to add to the admin group + * @param ePerson the eperson that will get the ADMIN permission on the item * @return this builder * @throws SQLException * @throws AuthorizeException diff --git a/dspace-api/src/test/java/org/dspace/builder/ProcessBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ProcessBuilder.java index 86573940e4..0631e1b55a 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ProcessBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ProcessBuilder.java @@ -113,6 +113,9 @@ public class ProcessBuilder extends AbstractBuilder { } public static void deleteProcess(Integer integer) throws SQLException, IOException { + if (integer == null) { + return; + } try (Context c = new Context()) { c.turnOffAuthorisationSystem(); Process process = processService.find(c, integer); diff --git a/dspace-api/src/test/java/org/dspace/scripts/MockDSpaceRunnableScriptConfiguration.java b/dspace-api/src/test/java/org/dspace/scripts/MockDSpaceRunnableScriptConfiguration.java index f69c0e3af7..632b4e2f83 100644 --- a/dspace-api/src/test/java/org/dspace/scripts/MockDSpaceRunnableScriptConfiguration.java +++ b/dspace-api/src/test/java/org/dspace/scripts/MockDSpaceRunnableScriptConfiguration.java @@ -8,21 +8,13 @@ package org.dspace.scripts; import java.io.InputStream; -import java.sql.SQLException; import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.impl.MockDSpaceRunnableScript; -import org.springframework.beans.factory.annotation.Autowired; public class MockDSpaceRunnableScriptConfiguration extends ScriptConfiguration { - - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -39,15 +31,6 @@ public class MockDSpaceRunnableScriptConfiguration> startProcess( @PathVariable(name = "name") String scriptName, @RequestParam(name = "file", required = false) List files) @@ -75,4 +77,13 @@ public class ScriptProcessesController { return ControllerUtils.toResponseEntity(HttpStatus.ACCEPTED, new HttpHeaders(), processResource); } + @RequestMapping(method = RequestMethod.POST, consumes = "!" + MediaType.MULTIPART_FORM_DATA_VALUE) + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public ResponseEntity> startProcessInvalidMimeType( + @PathVariable(name = "name") String scriptName, + @RequestParam(name = "file", required = false) List files) + throws Exception { + throw new DSpaceBadRequestException("Invalid mimetype"); + } + } 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 33addf7049..2479eeda97 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 @@ -94,6 +94,22 @@ public class ProcessRestRepository extends DSpaceRestRepository findByCurrentUser(Pageable pageable) { + + try { + Context context = obtainContext(); + long total = processService.countByUser(context, context.getCurrentUser()); + List processes = processService.findByUser(context, context.getCurrentUser(), + pageable.getPageSize(), + Math.toIntExact(pageable.getOffset())); + return converter.toRestPage(processes, pageable, total, utils.obtainProjection()); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + /** * Calls on the getBitstreams method to retrieve all the Bitstreams of this process * @param processId The processId of the Process to retrieve the Bitstreams for diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java index d974a6d78a..2fc996a327 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java @@ -37,6 +37,7 @@ import org.dspace.scripts.service.ScriptService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; @@ -56,29 +57,24 @@ public class ScriptRestRepository extends DSpaceRestRepository findAll(Context context, Pageable pageable) { List scriptConfigurations = scriptService.getScriptConfigurations(context); @@ -104,11 +100,17 @@ public class ScriptRestRepository extends DSpaceRestRepository dSpaceCommandLineParameters = processPropertiesToDSpaceCommandLineParameters(properties); ScriptConfiguration scriptToExecute = scriptService.getScriptConfiguration(scriptName); + if (scriptToExecute == null) { - throw new DSpaceBadRequestException("The script for name: " + scriptName + " wasn't found"); + throw new ResourceNotFoundException("The script for name: " + scriptName + " wasn't found"); } - if (!scriptToExecute.isAllowedToExecute(context)) { - throw new AuthorizeException("Current user is not eligible to execute script with name: " + scriptName); + try { + if (!scriptToExecute.isAllowedToExecute(context, dSpaceCommandLineParameters)) { + throw new AuthorizeException("Current user is not eligible to execute script with name: " + scriptName + + " and the specified parameters " + StringUtils.join(dSpaceCommandLineParameters, ", ")); + } + } catch (IllegalArgumentException e) { + throw new DSpaceBadRequestException("missed handle"); } RestDSpaceRunnableHandler restDSpaceRunnableHandler = new RestDSpaceRunnableHandler( context.getCurrentUser(), scriptToExecute.getName(), dSpaceCommandLineParameters, diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java index 5ac416e606..d76e20b23d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java @@ -7,6 +7,8 @@ */ package org.dspace.app.rest; +import static org.dspace.app.rest.matcher.ProcessMatcher.matchProcess; +import static org.dspace.content.ProcessStatus.SCHEDULED; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; @@ -783,6 +785,30 @@ public class ProcessRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isBadRequest()); } + @Test + public void testFindByCurrentUser() throws Exception { + + Process process1 = ProcessBuilder.createProcess(context, eperson, "mock-script", parameters) + .withStartAndEndTime("10/01/1990", "20/01/1990") + .build(); + ProcessBuilder.createProcess(context, admin, "mock-script", parameters) + .withStartAndEndTime("11/01/1990", "19/01/1990") + .build(); + Process process3 = ProcessBuilder.createProcess(context, eperson, "mock-script", parameters) + .withStartAndEndTime("12/01/1990", "18/01/1990") + .build(); + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(get("/api/system/processes/search/own")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.processes", contains( + matchProcess(process3.getName(), eperson.getID().toString(), process3.getID(), parameters, SCHEDULED), + matchProcess(process1.getName(), eperson.getID().toString(), process1.getID(), parameters, SCHEDULED)))) + .andExpect(jsonPath("$.page", is(PageMatcher.pageEntryWithTotalPagesAndElements(0, 20, 1, 2)))); + + } + @Test public void getProcessOutput() throws Exception { try (InputStream is = IOUtils.toInputStream("Test File For Process", CharEncoding.UTF_8)) { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java index 07edfeec33..16e691ef6f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java @@ -12,6 +12,7 @@ import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; @@ -44,6 +45,7 @@ import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.authorize.AuthorizeException; 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.ProcessBuilder; @@ -53,6 +55,7 @@ import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.content.ProcessStatus; import org.dspace.content.service.BitstreamService; +import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.scripts.DSpaceCommandLineParameter; import org.dspace.scripts.Process; @@ -123,12 +126,65 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { @Test - public void findAllScriptsUnauthorizedTest() throws Exception { + public void findAllScriptsGenericLoggedInUserTest() throws Exception { String token = getAuthToken(eperson.getEmail(), password); getClient(token).perform(get("/api/system/scripts")) - .andExpect(status().isForbidden()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + @Test + public void findAllScriptsLocalAdminsTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson comAdmin = EPersonBuilder.createEPerson(context) + .withEmail("comAdmin@example.com") + .withPassword(password).build(); + EPerson colAdmin = EPersonBuilder.createEPerson(context) + .withEmail("colAdmin@example.com") + .withPassword(password).build(); + EPerson itemAdmin = EPersonBuilder.createEPerson(context) + .withEmail("itemAdmin@example.com") + .withPassword(password).build(); + Community community = CommunityBuilder.createCommunity(context) + .withName("Community") + .withAdminGroup(comAdmin) + .build(); + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Collection") + .withAdminGroup(colAdmin) + .build(); + ItemBuilder.createItem(context, collection).withAdminUser(itemAdmin) + .withTitle("Test item to curate").build(); + context.restoreAuthSystemState(); + ScriptConfiguration curateScriptConfiguration = + scriptConfigurations.stream().filter(scriptConfiguration + -> scriptConfiguration.getName().equals("curate")) + .findAny().get(); + + // the local admins have at least access to the curate script + // and not access to process-cleaner script + String comAdminToken = getAuthToken(comAdmin.getEmail(), password); + getClient(comAdminToken).perform(get("/api/system/scripts").param("size", "100")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.scripts", Matchers.hasItem( + ScriptMatcher.matchScript(curateScriptConfiguration.getName(), + curateScriptConfiguration.getDescription())))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + String colAdminToken = getAuthToken(colAdmin.getEmail(), password); + getClient(colAdminToken).perform(get("/api/system/scripts").param("size", "100")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.scripts", Matchers.hasItem( + ScriptMatcher.matchScript(curateScriptConfiguration.getName(), + curateScriptConfiguration.getDescription())))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); + String itemAdminToken = getAuthToken(itemAdmin.getEmail(), password); + getClient(itemAdminToken).perform(get("/api/system/scripts").param("size", "100")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.scripts", Matchers.hasItem( + ScriptMatcher.matchScript(curateScriptConfiguration.getName(), + curateScriptConfiguration.getDescription())))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(1))); } @Test @@ -222,6 +278,63 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { )); } + @Test + public void findOneScriptByNameLocalAdminsTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson comAdmin = EPersonBuilder.createEPerson(context) + .withEmail("comAdmin@example.com") + .withPassword(password).build(); + EPerson colAdmin = EPersonBuilder.createEPerson(context) + .withEmail("colAdmin@example.com") + .withPassword(password).build(); + EPerson itemAdmin = EPersonBuilder.createEPerson(context) + .withEmail("itemAdmin@example.com") + .withPassword(password).build(); + Community community = CommunityBuilder.createCommunity(context) + .withName("Community") + .withAdminGroup(comAdmin) + .build(); + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Collection") + .withAdminGroup(colAdmin) + .build(); + ItemBuilder.createItem(context, collection).withAdminUser(itemAdmin) + .withTitle("Test item to curate").build(); + context.restoreAuthSystemState(); + ScriptConfiguration curateScriptConfiguration = + scriptConfigurations.stream().filter(scriptConfiguration + -> scriptConfiguration.getName().equals("curate")) + .findAny().get(); + + String comAdminToken = getAuthToken(comAdmin.getEmail(), password); + String colAdminToken = getAuthToken(colAdmin.getEmail(), password); + String itemAdminToken = getAuthToken(itemAdmin.getEmail(), password); + getClient(comAdminToken).perform(get("/api/system/scripts/" + curateScriptConfiguration.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", ScriptMatcher + .matchScript( + curateScriptConfiguration.getName(), + curateScriptConfiguration.getDescription()))); + getClient(colAdminToken).perform(get("/api/system/scripts/" + curateScriptConfiguration.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", ScriptMatcher + .matchScript( + curateScriptConfiguration.getName(), + curateScriptConfiguration.getDescription()))); + getClient(itemAdminToken).perform(get("/api/system/scripts/" + curateScriptConfiguration.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", ScriptMatcher + .matchScript( + curateScriptConfiguration.getName(), + curateScriptConfiguration.getDescription()))); + } + + @Test + public void findOneScriptByNameNotAuthenticatedTest() throws Exception { + getClient().perform(get("/api/system/scripts/mock-script")) + .andExpect(status().isUnauthorized()); + } + @Test public void findOneScriptByNameTestAccessDenied() throws Exception { String token = getAuthToken(eperson.getEmail(), password); @@ -235,7 +348,7 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { String token = getAuthToken(admin.getEmail(), password); getClient(token).perform(get("/api/system/scripts/mock-script-invalid")) - .andExpect(status().isBadRequest()); + .andExpect(status().isNotFound()); } @Test @@ -277,16 +390,6 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void postProcessAdminNoOptionsFailedStatus() throws Exception { -// List list = new LinkedList<>(); -// -// ParameterValueRest parameterValueRest = new ParameterValueRest(); -// parameterValueRest.setName("-z"); -// parameterValueRest.setValue("test"); -// ParameterValueRest parameterValueRest1 = new ParameterValueRest(); -// parameterValueRest1.setName("-q"); -// list.add(parameterValueRest); -// list.add(parameterValueRest1); - LinkedList parameters = new LinkedList<>(); parameters.add(new DSpaceCommandLineParameter("-z", "test")); @@ -322,7 +425,7 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { String token = getAuthToken(admin.getEmail(), password); getClient(token).perform(multipart("/api/system/scripts/mock-script-invalid/processes")) - .andExpect(status().isBadRequest()); + .andExpect(status().isNotFound()); } @Test @@ -434,6 +537,8 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { } + + @Test public void postProcessAdminWithWrongContentTypeBadRequestException() throws Exception { @@ -601,9 +706,9 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { } } - @After public void destroy() throws Exception { + context.turnOffAuthorisationSystem(); CollectionUtils.emptyIfNull(processService.findAll(context)).stream().forEach(process -> { try { processService.delete(context, process); @@ -611,6 +716,7 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { throw new RuntimeException(e); } }); + context.restoreAuthSystemState(); super.destroy(); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScriptConfiguration.java b/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScriptConfiguration.java index 27c37f1487..ccb7d43a23 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScriptConfiguration.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/scripts/TypeConversionTestScriptConfiguration.java @@ -11,7 +11,6 @@ import java.io.InputStream; import org.apache.commons.cli.Options; import org.dspace.app.rest.converter.ScriptConverter; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; /** @@ -28,10 +27,6 @@ public class TypeConversionTestScriptConfiguration siteParameters = new LinkedList<>(); + siteParameters.add(new DSpaceCommandLineParameter("-i", site.getHandle())); + siteParameters.add(new DSpaceCommandLineParameter("-t", "noop")); + LinkedList comParameters = new LinkedList<>(); + comParameters.add(new DSpaceCommandLineParameter("-i", community.getHandle())); + comParameters.add(new DSpaceCommandLineParameter("-t", "noop")); + LinkedList anotherComParameters = new LinkedList<>(); + anotherComParameters.add(new DSpaceCommandLineParameter("-i", anotherCommunity.getHandle())); + anotherComParameters.add(new DSpaceCommandLineParameter("-t", "noop")); + LinkedList colParameters = new LinkedList<>(); + colParameters.add(new DSpaceCommandLineParameter("-i", collection.getHandle())); + colParameters.add(new DSpaceCommandLineParameter("-t", "noop")); + LinkedList anotherColParameters = new LinkedList<>(); + anotherColParameters.add(new DSpaceCommandLineParameter("-i", anotherCollection.getHandle())); + anotherColParameters.add(new DSpaceCommandLineParameter("-t", "noop")); + LinkedList itemParameters = new LinkedList<>(); + itemParameters.add(new DSpaceCommandLineParameter("-i", item.getHandle())); + itemParameters.add(new DSpaceCommandLineParameter("-t", "noop")); + LinkedList anotherItemParameters = new LinkedList<>(); + anotherItemParameters.add(new DSpaceCommandLineParameter("-i", anotherItem.getHandle())); + anotherItemParameters.add(new DSpaceCommandLineParameter("-t", "noop")); + String comAdminToken = getAuthToken(comAdmin.getEmail(), password); + String colAdminToken = getAuthToken(colAdmin.getEmail(), password); + String itemAdminToken = getAuthToken(itemAdmin.getEmail(), password); + + List listCurateSite = siteParameters.stream() + .map(dSpaceCommandLineParameter -> dSpaceRunnableParameterConverter + .convert(dSpaceCommandLineParameter, Projection.DEFAULT)) + .collect(Collectors.toList()); + List listCom = comParameters.stream() + .map(dSpaceCommandLineParameter -> dSpaceRunnableParameterConverter + .convert(dSpaceCommandLineParameter, Projection.DEFAULT)) + .collect(Collectors.toList()); + List listAnotherCom = anotherComParameters.stream() + .map(dSpaceCommandLineParameter -> dSpaceRunnableParameterConverter + .convert(dSpaceCommandLineParameter, Projection.DEFAULT)) + .collect(Collectors.toList()); + List listCol = colParameters.stream() + .map(dSpaceCommandLineParameter -> dSpaceRunnableParameterConverter + .convert(dSpaceCommandLineParameter, Projection.DEFAULT)) + .collect(Collectors.toList()); + List listAnotherCol = anotherColParameters.stream() + .map(dSpaceCommandLineParameter -> dSpaceRunnableParameterConverter + .convert(dSpaceCommandLineParameter, Projection.DEFAULT)) + .collect(Collectors.toList()); + List listItem = itemParameters.stream() + .map(dSpaceCommandLineParameter -> dSpaceRunnableParameterConverter + .convert(dSpaceCommandLineParameter, Projection.DEFAULT)) + .collect(Collectors.toList()); + List listAnotherItem = anotherItemParameters.stream() + .map(dSpaceCommandLineParameter -> dSpaceRunnableParameterConverter + .convert(dSpaceCommandLineParameter, Projection.DEFAULT)) + .collect(Collectors.toList()); + String adminToken = getAuthToken(admin.getEmail(), password); + List acceptableProcessStatuses = new LinkedList<>(); + acceptableProcessStatuses.addAll(Arrays.asList(ProcessStatus.SCHEDULED, + ProcessStatus.RUNNING, + ProcessStatus.COMPLETED)); + + AtomicReference idSiteRef = new AtomicReference<>(); + AtomicReference idComRef = new AtomicReference<>(); + AtomicReference idComColRef = new AtomicReference<>(); + AtomicReference idComItemRef = new AtomicReference<>(); + AtomicReference idColRef = new AtomicReference<>(); + AtomicReference idColItemRef = new AtomicReference<>(); + AtomicReference idItemRef = new AtomicReference<>(); + + ScriptConfiguration curateScriptConfiguration = scriptService.getScriptConfiguration("curate"); + // we should be able to start the curate script with all our admins on the respective dso + try { + // start a process as general admin + getClient(adminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCurateSite))) + .andExpect(status().isAccepted()) + .andExpect(jsonPath("$", is( + ProcessMatcher.matchProcess("curate", + String.valueOf(admin.getID()), + siteParameters, + acceptableProcessStatuses)))) + .andDo(result -> idSiteRef + .set(read(result.getResponse().getContentAsString(), "$.processId"))); + + // check with the com admin + getClient(comAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCom))) + .andExpect(status().isAccepted()) + .andExpect(jsonPath("$", is( + ProcessMatcher.matchProcess("curate", + String.valueOf(comAdmin.getID()), + comParameters, + acceptableProcessStatuses)))) + .andDo(result -> idComRef + .set(read(result.getResponse().getContentAsString(), "$.processId"))); + // the com admin should be able to run the curate also over the children collection and item + getClient(comAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCol))) + .andExpect(status().isAccepted()) + .andExpect(jsonPath("$", is( + ProcessMatcher.matchProcess("curate", + String.valueOf(comAdmin.getID()), + colParameters, + acceptableProcessStatuses)))) + .andDo(result -> idComColRef + .set(read(result.getResponse().getContentAsString(), "$.processId"))); + getClient(comAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listItem))) + .andExpect(status().isAccepted()) + .andExpect(jsonPath("$", is( + ProcessMatcher.matchProcess("curate", + String.valueOf(comAdmin.getID()), + itemParameters, + acceptableProcessStatuses)))) + .andDo(result -> idComItemRef + .set(read(result.getResponse().getContentAsString(), "$.processId"))); + // the com admin should be NOT able to run the curate over other com, col or items + getClient(comAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCurateSite))) + .andExpect(status().isForbidden()); + getClient(comAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listAnotherCom))) + .andExpect(status().isForbidden()); + getClient(comAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listAnotherCol))) + .andExpect(status().isForbidden()); + getClient(comAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listAnotherItem))) + .andExpect(status().isForbidden()); + + // check with the col admin + getClient(colAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCol))) + .andExpect(status().isAccepted()) + .andExpect(jsonPath("$", is( + ProcessMatcher.matchProcess("curate", + String.valueOf(colAdmin.getID()), + colParameters, + acceptableProcessStatuses)))) + .andDo(result -> idColRef + .set(read(result.getResponse().getContentAsString(), "$.processId"))); + // the col admin should be able to run the curate also over the owned item + getClient(colAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listItem))) + .andExpect(status().isAccepted()) + .andExpect(jsonPath("$", is( + ProcessMatcher.matchProcess("curate", + String.valueOf(colAdmin.getID()), + itemParameters, + acceptableProcessStatuses)))) + .andDo(result -> idColItemRef + .set(read(result.getResponse().getContentAsString(), "$.processId"))); + + // the col admin should be NOT able to run the curate over the community nor another collection nor + // on a not owned item + getClient(colAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCurateSite))) + .andExpect(status().isForbidden()); + getClient(colAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCom))) + .andExpect(status().isForbidden()); + getClient(colAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listAnotherCol))) + .andExpect(status().isForbidden()); + getClient(colAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listAnotherItem))) + .andExpect(status().isForbidden()); + + // check with the item admin + getClient(itemAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listItem))) + .andExpect(status().isAccepted()) + .andExpect(jsonPath("$", is( + ProcessMatcher.matchProcess("curate", + String.valueOf(itemAdmin.getID()), + itemParameters, + acceptableProcessStatuses)))) + .andDo(result -> idItemRef + .set(read(result.getResponse().getContentAsString(), "$.processId"))); + // the item admin should be NOT able to run the curate over the community nor the collection nor + // on a not owned item + getClient(itemAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCurateSite))) + .andExpect(status().isForbidden()); + getClient(itemAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCom))) + .andExpect(status().isForbidden()); + getClient(itemAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listCol))) + .andExpect(status().isForbidden()); + getClient(itemAdminToken) + .perform(multipart("/api/system/scripts/" + curateScriptConfiguration.getName() + "/processes") + .param("properties", new ObjectMapper().writeValueAsString(listAnotherItem))) + .andExpect(status().isForbidden()); + + } finally { + ProcessBuilder.deleteProcess(idSiteRef.get()); + ProcessBuilder.deleteProcess(idComRef.get()); + ProcessBuilder.deleteProcess(idComColRef.get()); + ProcessBuilder.deleteProcess(idComItemRef.get()); + ProcessBuilder.deleteProcess(idColRef.get()); + ProcessBuilder.deleteProcess(idColItemRef.get()); + ProcessBuilder.deleteProcess(idItemRef.get()); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/scripts/MockDSpaceRunnableScriptConfiguration.java b/dspace-server-webapp/src/test/java/org/dspace/scripts/MockDSpaceRunnableScriptConfiguration.java index f69c0e3af7..632b4e2f83 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/scripts/MockDSpaceRunnableScriptConfiguration.java +++ b/dspace-server-webapp/src/test/java/org/dspace/scripts/MockDSpaceRunnableScriptConfiguration.java @@ -8,21 +8,13 @@ package org.dspace.scripts; import java.io.InputStream; -import java.sql.SQLException; import org.apache.commons.cli.Options; -import org.dspace.authorize.service.AuthorizeService; -import org.dspace.core.Context; import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.impl.MockDSpaceRunnableScript; -import org.springframework.beans.factory.annotation.Autowired; public class MockDSpaceRunnableScriptConfiguration extends ScriptConfiguration { - - @Autowired - private AuthorizeService authorizeService; - private Class dspaceRunnableClass; @Override @@ -39,15 +31,6 @@ public class MockDSpaceRunnableScriptConfiguration Date: Fri, 5 May 2023 18:45:52 +0200 Subject: [PATCH 29/63] 100553: Added test for create metadata schema & field and created test for sort byFieldName --- .../MetadataFieldRestRepository.java | 19 ++- .../MetadataSchemaRestRepository.java | 6 +- .../rest/MetadataSchemaRestRepositoryIT.java | 23 +++- .../rest/MetadatafieldRestRepositoryIT.java | 114 +++++++++++++++++- 4 files changed, 149 insertions(+), 13 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java index c185e83342..eefd6331d1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java @@ -14,6 +14,7 @@ import static org.dspace.app.rest.model.SearchConfigurationRest.Filter.OPERATOR_ import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.Objects; import javax.servlet.http.HttpServletRequest; @@ -45,10 +46,10 @@ import org.dspace.discovery.indexobject.MetadataFieldIndexFactoryImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; - /** * This is the repository responsible to manage MetadataField Rest object * @@ -213,7 +214,13 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository orderIterator = pageable.getSort().iterator(); + if (orderIterator.hasNext()) { + Sort.Order order = orderIterator.next(); + discoverQuery.setSortField(order.getProperty() + "_sort", + order.getDirection() == Sort.Direction.ASC ? DiscoverQuery.SORT_ORDER.asc : + DiscoverQuery.SORT_ORDER.desc); + } discoverQuery.setStart(Math.toIntExact(pageable.getOffset())); discoverQuery.setMaxResults(pageable.getPageSize()); return discoverQuery; @@ -254,13 +261,13 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository idRef = new AtomicReference<>(); try { - assertThat(metadataFieldService.findByElement(context, metadataSchema, ELEMENT, QUALIFIER), nullValue()); + assertThat(metadataFieldService.findByElement(context, metadataSchema, metadataFieldRest.getElement(), + metadataFieldRest.getQualifier()), nullValue()); getClient(authToken) .perform(post("/api/core/metadatafields") @@ -606,7 +672,8 @@ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegration String authToken = getAuthToken(admin.getEmail(), password); Integer id = null; try { - assertThat(metadataFieldService.findByElement(context, metadataSchema, ELEMENT, null), nullValue()); + assertThat(metadataFieldService.findByElement(context, metadataSchema, metadataFieldRest.getElement(), + null), nullValue()); id = read( getClient(authToken) @@ -641,7 +708,8 @@ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegration String authToken = getAuthToken(admin.getEmail(), password); AtomicReference idRef = new AtomicReference<>(); try { - assertThat(metadataFieldService.findByElement(context, metadataSchema, ELEMENT, QUALIFIER), nullValue()); + assertThat(metadataFieldService.findByElement(context, metadataSchema, metadataFieldRest.getElement(), + metadataFieldRest.getQualifier()), nullValue()); getClient(authToken) .perform(post("/api/core/metadatafields") @@ -689,6 +757,46 @@ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegration .andExpect(status().isUnauthorized()); } + @Test + public void createUnprocessableEntity_elementContainingDots() throws Exception { + MetadataFieldRest metadataFieldRest = new MetadataFieldRest(); + metadataFieldRest.setElement("testElement.ForCreate"); + metadataFieldRest.setQualifier(QUALIFIER); + metadataFieldRest.setScopeNote(SCOPE_NOTE); + + String authToken = getAuthToken(admin.getEmail(), password); + assertThat(metadataFieldService.findByElement(context, metadataSchema, metadataFieldRest.getElement(), + metadataFieldRest.getQualifier()), nullValue()); + + getClient(authToken) + .perform(post("/api/core/metadatafields") + .param("schemaId", String.valueOf(metadataSchema.getID())) + .param("projection", "full") + .content(new ObjectMapper().writeValueAsBytes(metadataFieldRest)) + .contentType(contentType)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void createUnprocessableEntity_qualifierContainingDots() throws Exception { + MetadataFieldRest metadataFieldRest = new MetadataFieldRest(); + metadataFieldRest.setElement(ELEMENT); + metadataFieldRest.setQualifier("testQualifier.ForCreate"); + metadataFieldRest.setScopeNote(SCOPE_NOTE); + + String authToken = getAuthToken(admin.getEmail(), password); + assertThat(metadataFieldService.findByElement(context, metadataSchema, metadataFieldRest.getElement(), + metadataFieldRest.getQualifier()), nullValue()); + + getClient(authToken) + .perform(post("/api/core/metadatafields") + .param("schemaId", String.valueOf(metadataSchema.getID())) + .param("projection", "full") + .content(new ObjectMapper().writeValueAsBytes(metadataFieldRest)) + .contentType(contentType)) + .andExpect(status().isUnprocessableEntity()); + } + @Test public void createUnauthorizedEPersonNoAdminRights() throws Exception { From c670251a68433cfaecfa73f65665b9892aef22fe Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Thu, 11 May 2023 17:13:13 +0200 Subject: [PATCH 30/63] 94299: Fix minor issues --- .../rest/BitstreamCategoryRestController.java | 1 - .../operation/BitstreamRemoveOperation.java | 15 +- .../app/rest/BitstreamRestRepositoryIT.java | 299 ++++++++++++++++++ 3 files changed, 313 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java index 13929e5a9a..6d970eb109 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java @@ -51,7 +51,6 @@ public class BitstreamCategoryRestController { * @throws SQLException if an error occurs while accessing the database. * @throws AuthorizeException if the user is not authorized to perform the requested operation. */ - @PreAuthorize("hasAuthority('ADMIN')") @RequestMapping(method = RequestMethod.PATCH) public ResponseEntity> patch(HttpServletRequest request, @RequestBody(required = true) JsonNode jsonNode) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java index 93c495a302..7733600271 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java @@ -14,10 +14,13 @@ import java.util.UUID; import org.dspace.app.rest.exception.RESTBitstreamNotFoundException; import org.dspace.app.rest.model.patch.Operation; import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bitstream; import org.dspace.content.service.BitstreamService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Component; /** @@ -38,6 +41,8 @@ import org.springframework.stereotype.Component; public class BitstreamRemoveOperation extends PatchOperation { @Autowired BitstreamService bitstreamService; + @Autowired + AuthorizeService authorizeService; public static final String OPERATION_PATH_BITSTREAM_REMOVE = "/bitstreams/"; @Override @@ -47,10 +52,10 @@ public class BitstreamRemoveOperation extends PatchOperation { if (bitstreamToDelete == null) { throw new RESTBitstreamNotFoundException(bitstreamIDtoDelete); } + authorizeBitstreamRemoveAction(context, bitstreamToDelete, Constants.DELETE); try { bitstreamService.delete(context, bitstreamToDelete); - bitstreamService.update(context, bitstreamToDelete); } catch (AuthorizeException | IOException e) { throw new RuntimeException(e.getMessage(), e); } @@ -62,4 +67,12 @@ public class BitstreamRemoveOperation extends PatchOperation { return objectToMatch == null && operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && operation.getPath().trim().startsWith(OPERATION_PATH_BITSTREAM_REMOVE); } + + public void authorizeBitstreamRemoveAction(Context context, Bitstream bitstream, int operation) throws SQLException { + try { + authorizeService.authorizeAction(context, bitstream, operation); + } catch (AuthorizeException e) { + throw new AccessDeniedException("The current user is not allowed to remove the bitstream", e); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index 3b01b4eac2..2d855a06c2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -46,6 +46,7 @@ import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.BundleBuilder; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.builder.ResourcePolicyBuilder; import org.dspace.content.Bitstream; @@ -56,6 +57,8 @@ import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.content.service.BitstreamFormatService; import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.CollectionService; +import org.dspace.content.service.CommunityService; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.eperson.EPerson; @@ -86,6 +89,12 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest @Autowired private ItemService itemService; + @Autowired + CollectionService collectionService; + + @Autowired + CommunityService communityService; + @Test public void findAllTest() throws Exception { //We turn off the authorization system in order to create the structure as defined below @@ -2490,6 +2499,296 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest Assert.assertTrue(bitstreamExists(token, bitstream1, bitstream2, bitstream3, bitstream4)); } + @Test + public void deleteBitstreamsInBulk_Unauthorized() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + Item publicItem1 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 1") + .build(); + Item publicItem2 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 2") + .build(); + + String bitstreamContent = "This is an archived bitstream"; + Bitstream bitstream1 = null; + Bitstream bitstream2 = null; + Bitstream bitstream3 = null; + Bitstream bitstream4 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 1") + .withMimeType("text/plain") + .build(); + bitstream2 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 2") + .withMimeType("text/plain") + .build(); + bitstream3 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 3") + .withMimeType("text/plain") + .build(); + bitstream4 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 4") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + // Add three out of four bitstreams to the list of bitstreams to be deleted + List ops = new ArrayList<>(); + RemoveOperation removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream1.getID()); + ops.add(removeOp1); + RemoveOperation removeOp2 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream2.getID()); + ops.add(removeOp2); + RemoveOperation removeOp3 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream3.getID()); + ops.add(removeOp3); + String patchBody = getPatchContent(ops); + String token = getAuthToken(admin.getEmail(), password); + + Assert.assertTrue(bitstreamExists(token, bitstream1, bitstream2, bitstream3, bitstream4)); + + getClient().perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void deleteBitstreamsInBulk_Forbidden() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + Item publicItem1 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 1") + .build(); + Item publicItem2 = ItemBuilder.createItem(context, collection) + .withTitle("Test item 2") + .build(); + + String bitstreamContent = "This is an archived bitstream"; + Bitstream bitstream1 = null; + Bitstream bitstream2 = null; + Bitstream bitstream3 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 1") + .withMimeType("text/plain") + .build(); + bitstream2 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 2") + .withMimeType("text/plain") + .build(); + bitstream3 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 3") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + // Add three out of four bitstreams to the list of bitstreams to be deleted + List ops = new ArrayList<>(); + RemoveOperation removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream1.getID()); + ops.add(removeOp1); + RemoveOperation removeOp2 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream2.getID()); + ops.add(removeOp2); + RemoveOperation removeOp3 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream3.getID()); + ops.add(removeOp3); + String patchBody = getPatchContent(ops); + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + public void deleteBitstreamsInBulk_collectionAdmin() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + Collection col2 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 2") + .build(); + EPerson col1Admin = EPersonBuilder.createEPerson(context) + .withEmail("col1admin@test.com") + .withPassword(password) + .build(); + EPerson col2Admin = EPersonBuilder.createEPerson(context) + .withEmail("col2admin@test.com") + .withPassword(password) + .build(); + Group col1_AdminGroup = collectionService.createAdministrators(context, col1); + Group col2_AdminGroup = collectionService.createAdministrators(context, col2); + groupService.addMember(context, col1_AdminGroup, col1Admin); + groupService.addMember(context, col2_AdminGroup, col2Admin); + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test item 1") + .build(); + Item publicItem2 = ItemBuilder.createItem(context, col2) + .withTitle("Test item 2") + .build(); + + String bitstreamContent = "This is an archived bitstream"; + Bitstream bitstream1 = null; + Bitstream bitstream2 = null; + Bitstream bitstream3 = null; + Bitstream bitstream4 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 1") + .withMimeType("text/plain") + .build(); + bitstream2 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 2") + .withMimeType("text/plain") + .build(); + bitstream3 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 3") + .withMimeType("text/plain") + .build(); + bitstream4 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 4") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + // Add three out of four bitstreams to the list of bitstreams to be deleted + List ops = new ArrayList<>(); + RemoveOperation removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream1.getID()); + ops.add(removeOp1); + RemoveOperation removeOp2 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream2.getID()); + ops.add(removeOp2); + RemoveOperation removeOp3 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream3.getID()); + ops.add(removeOp3); + String patchBody = getPatchContent(ops); + + String token = getAuthToken(col1Admin.getEmail(), password); + // Should return forbidden since one of the bitstreams does not originate form collection 1 + getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isForbidden()); + + // Remove the bitstream that does not originate from the collection we are administrator of, should return OK + ops.remove(2); + patchBody = getPatchContent(ops); + getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isNoContent()); + + // Change the token to the admin of collection 2 + token = getAuthToken(col2Admin.getEmail(), password); + + // Add three out of four bitstreams to the list of bitstreams to be deleted + ops = new ArrayList<>(); + removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream2.getID()); + ops.add(removeOp1); + removeOp2 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream3.getID()); + ops.add(removeOp2); + removeOp3 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream4.getID()); + ops.add(removeOp3); + patchBody = getPatchContent(ops); + + // Should return forbidden since one of the bitstreams does not originate form collection 2 + getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isForbidden()); + // Remove the bitstream that does not originate from the collection we are administrator of, should return OK + ops.remove(0); + patchBody = getPatchContent(ops); + getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isNoContent()); + } + + @Test + public void deleteBitstreamsInBulk_communityAdmin() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + Collection col2 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 2") + .build(); + EPerson parentCommunityAdmin = EPersonBuilder.createEPerson(context) + .withEmail("parentComAdmin@test.com") + .withPassword(password) + .build(); + Group parentComAdminGroup = communityService.createAdministrators(context, parentCommunity); + groupService.addMember(context, parentComAdminGroup, parentCommunityAdmin); + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test item 1") + .build(); + Item publicItem2 = ItemBuilder.createItem(context, col2) + .withTitle("Test item 2") + .build(); + + String bitstreamContent = "This is an archived bitstream"; + Bitstream bitstream1 = null; + Bitstream bitstream2 = null; + Bitstream bitstream3 = null; + Bitstream bitstream4 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 1") + .withMimeType("text/plain") + .build(); + bitstream2 = BitstreamBuilder.createBitstream(context, publicItem1, is) + .withName("Bitstream 2") + .withMimeType("text/plain") + .build(); + bitstream3 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 3") + .withMimeType("text/plain") + .build(); + bitstream4 = BitstreamBuilder.createBitstream(context, publicItem2, is) + .withName("Bitstream 4") + .withMimeType("text/plain") + .build(); + } + context.restoreAuthSystemState(); + + // Add three out of four bitstreams to the list of bitstreams to be deleted + List ops = new ArrayList<>(); + RemoveOperation removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream1.getID()); + ops.add(removeOp1); + RemoveOperation removeOp2 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream2.getID()); + ops.add(removeOp2); + RemoveOperation removeOp3 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream3.getID()); + ops.add(removeOp3); + String patchBody = getPatchContent(ops); + + String token = getAuthToken(parentCommunityAdmin.getEmail(), password); + // Bitstreams originate from two different collections, but those collections live in the same community, so + // a community admin should be able to delete them + getClient(token).perform(patch("/api/core/bitstreams") + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isNoContent()); + } + public boolean bitstreamExists(String token, Bitstream ...bitstreams) throws Exception { for (Bitstream bitstream : bitstreams) { if (getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID())) From 7c7824f913c5db9cffafb5afed8394cc441d5c8c Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 11 May 2023 17:57:45 +0200 Subject: [PATCH 31/63] Implement community feedback --- .../DiscoveryConfigurationService.java | 3 +- .../DiscoveryScopeBasedRestControllerIT.java | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) 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 da23b87a35..1a1ed95a29 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 @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -42,7 +43,7 @@ public class DiscoveryConfigurationService { * own configuration, we take the one of the first parent that does. * This cache ensures we do not have to go up the hierarchy every time. */ - private final Map comColToDiscoveryConfigurationMap = new HashMap<>(); + private final Map comColToDiscoveryConfigurationMap = new ConcurrentHashMap<>(); public Map getMap() { return map; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java index 0c8735545e..a3408a7736 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryScopeBasedRestControllerIT.java @@ -58,6 +58,9 @@ import org.springframework.beans.factory.annotation.Autowired; * * The tests will verify that for each object, the correct facets are provided and that all the necessary fields to * power these facets are indexed properly. + * + * This file requires the discovery configuration in the following test file: + * src/test/data/dspaceFolder/config/spring/api/test-discovery.xml */ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerIntegrationTest { @@ -263,6 +266,9 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the custom configuration "discovery-parent-community-1" is correctly used for Parent Community 1. + */ public void ScopeBasedIndexingAndSearchTestParentCommunity1() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(parentCommunity1.getID()))) @@ -301,6 +307,9 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the custom configuration "discovery-sub-community-1-1" is correctly used for Subcommunity 11. + */ public void ScopeBasedIndexingAndSearchTestSubCommunity11() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(subcommunity11.getID()))) @@ -330,6 +339,9 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the custom configuration "discovery-collection-1-1-1" is correctly used for Collection 111. + */ public void ScopeBasedIndexingAndSearchTestCollection111() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection111.getID()))) @@ -357,6 +369,10 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the first encountered custom parent configuration "discovery-sub-community-1-1" is inherited + * correctly for Collection 112. + */ public void ScopeBasedIndexingAndSearchTestCollection112() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection112.getID()))) @@ -382,6 +398,10 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the first encountered custom parent configuration "discovery-parent-community-1" is inherited + * correctly for Subcommunity 12. + */ public void ScopeBasedIndexingAndSearchTestSubcommunity12() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(subcommunity12.getID()))) @@ -411,6 +431,9 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the custom configuration "discovery-collection-1-2-1" is correctly used for Collection 121. + */ public void ScopeBasedIndexingAndSearchTestCollection121() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection121.getID()))) @@ -436,6 +459,10 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the first encountered custom parent configuration "discovery-parent-community-1" is inherited + * correctly for Collection 122. + */ public void ScopeBasedIndexingAndSearchTestCollection122() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection122.getID()))) @@ -463,6 +490,10 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the default configuration is inherited correctly when no other custom configuration can be inherited + * for Parent Community 2. + */ public void ScopeBasedIndexingAndSearchTestParentCommunity2() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(parentCommunity2.getID()))) @@ -481,6 +512,9 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the custom configuration "discovery-sub-community-2-1" is correctly used for Subcommunity 21. + */ public void ScopeBasedIndexingAndSearchTestSubCommunity21() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(subcommunity21.getID()))) @@ -510,6 +544,9 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the custom configuration "discovery-collection-2-1-1" is correctly used for Collection 211. + */ public void ScopeBasedIndexingAndSearchTestCollection211() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection211.getID()))) @@ -537,6 +574,10 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the first encountered custom parent configuration "discovery-sub-community-2-1" is inherited + * correctly for Collection 212. + */ public void ScopeBasedIndexingAndSearchTestCollection212() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection212.getID()))) @@ -562,6 +603,10 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the default configuration is inherited correctly when no other custom configuration can be inherited + * for Subcommunity 22. + */ public void ScopeBasedIndexingAndSearchTestSubcommunity22() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(subcommunity22.getID()))) @@ -579,6 +624,9 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the custom configuration "discovery-collection-2-2-1" is correctly used for Collection 221. + */ public void ScopeBasedIndexingAndSearchTestCollection221() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection221.getID()))) @@ -604,6 +652,10 @@ public class DiscoveryScopeBasedRestControllerIT extends AbstractControllerInteg } @Test + /** + * Verify that the default configuration is inherited correctly when no other custom configuration can be inherited + * for Collection 222. + */ public void ScopeBasedIndexingAndSearchTestCollection222() throws Exception { getClient().perform(get("/api/discover/facets").param("scope", String.valueOf(collection222.getID()))) From 78fba6b579c3af233c80861c6efb90491cb8d925 Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Thu, 11 May 2023 21:24:31 +0200 Subject: [PATCH 32/63] 94299: checkstyle --- .../org/dspace/app/rest/BitstreamCategoryRestController.java | 1 - .../repository/patch/operation/BitstreamRemoveOperation.java | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java index 6d970eb109..aa511bcb92 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/BitstreamCategoryRestController.java @@ -20,7 +20,6 @@ import org.dspace.core.Context; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.RepresentationModel; import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java index 7733600271..b0e2a45c9d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/BitstreamRemoveOperation.java @@ -68,7 +68,8 @@ public class BitstreamRemoveOperation extends PatchOperation { operation.getPath().trim().startsWith(OPERATION_PATH_BITSTREAM_REMOVE); } - public void authorizeBitstreamRemoveAction(Context context, Bitstream bitstream, int operation) throws SQLException { + public void authorizeBitstreamRemoveAction(Context context, Bitstream bitstream, int operation) + throws SQLException { try { authorizeService.authorizeAction(context, bitstream, operation); } catch (AuthorizeException e) { From b24f121c767c1803882f1ae03d91d49e5a751eed Mon Sep 17 00:00:00 2001 From: Jens Vannerum Date: Fri, 12 May 2023 10:11:09 +0200 Subject: [PATCH 33/63] 94299: checkstyle issue after main merge --- .../java/org/dspace/app/rest/BitstreamRestRepositoryIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index 0b65f3e4b9..2a1044c28a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -37,10 +37,10 @@ import org.dspace.app.rest.matcher.BitstreamFormatMatcher; import org.dspace.app.rest.matcher.BitstreamMatcher; import org.dspace.app.rest.matcher.BundleMatcher; import org.dspace.app.rest.matcher.HalMatcher; +import org.dspace.app.rest.matcher.MetadataMatcher; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.RemoveOperation; import org.dspace.app.rest.model.patch.ReplaceOperation; -import org.dspace.app.rest.matcher.MetadataMatcher; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; import org.dspace.authorize.service.ResourcePolicyService; From d7d7f7c37034f6c571c1a61d5ba9afdc71d91d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Fri, 19 May 2023 22:44:07 +0100 Subject: [PATCH 34/63] support for entity type for collection at input submissions --- .../app/util/SubmissionConfigReader.java | 26 ++++++++++++++++--- .../dspace/content/CollectionServiceImpl.java | 22 ++++++++++++++++ .../content/service/CollectionService.java | 15 +++++++++++ dspace/config/item-submission.dtd | 3 ++- dspace/config/item-submission.xml | 20 ++++++++++++++ 5 files changed, 82 insertions(+), 4 deletions(-) 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 2120848358..7132c1e934 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 @@ -22,6 +22,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.content.Collection; import org.dspace.content.DSpaceObject; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.CollectionService; import org.dspace.core.Context; import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.services.factory.DSpaceServicesFactory; @@ -104,6 +106,9 @@ public class SubmissionConfigReader { * always reload from scratch) */ private SubmissionConfig lastSubmissionConfig = null; + + protected static final CollectionService collectionService + = ContentServiceFactory.getInstance().getCollectionService(); /** * Load Submission Configuration from the @@ -335,17 +340,22 @@ public class SubmissionConfigReader { * by the collection handle. */ private void processMap(Node e) throws SAXException { + // create a context + Context context = new Context(); + NodeList nl = e.getChildNodes(); int len = nl.getLength(); for (int i = 0; i < len; i++) { Node nd = nl.item(i); if (nd.getNodeName().equals("name-map")) { String id = getAttribute(nd, "collection-handle"); + String entityType = getAttribute(nd, "collection-entity-type"); String value = getAttribute(nd, "submission-name"); String content = getValue(nd); - if (id == null) { + if (id == null && entityType == null) { throw new SAXException( - "name-map element is missing collection-handle attribute in 'item-submission.xml'"); + "name-map element is missing collection-handle or collection-entity-type attribute " + + "in 'item-submission.xml'"); } if (value == null) { throw new SAXException( @@ -355,7 +365,17 @@ public class SubmissionConfigReader { throw new SAXException( "name-map element has content in 'item-submission.xml', it should be empty."); } - collectionToSubmissionConfig.put(id, value); + if (id != null) { + collectionToSubmissionConfig.put(id, value); + + } else { + // get all collections for this entity-type + List collections = collectionService.findAllCollectionsByEntityType( context, + entityType); + for (Collection collection : collections) { + collectionToSubmissionConfig.putIfAbsent(collection.getHandle(), value); + } + } } // ignore any child node that isn't a "name-map" } } 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 ddfd38694f..ef89009ebf 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -1047,4 +1048,25 @@ public class CollectionServiceImpl extends DSpaceObjectServiceImpl i return (int) resp.getTotalSearchResults(); } + @Override + @SuppressWarnings("rawtypes") + public List findAllCollectionsByEntityType(Context context, String entityType) + throws SearchServiceException { + List collectionList = new ArrayList<>(); + + DiscoverQuery discoverQuery = new DiscoverQuery(); + discoverQuery.setMaxResults(0); + discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE); + discoverQuery.addFilterQueries("dspace.entity.type:" + entityType); + + DiscoverResult discoverResult = searchService.search(context, discoverQuery); + List solrIndexableObjects = discoverResult.getIndexableObjects(); + + for (IndexableObject solrCollection : solrIndexableObjects) { + Collection c = ((IndexableCollection) solrCollection).getIndexedObject(); + collectionList.add(c); + } + return collectionList; + } + } 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 a5b2b7d8d8..82d8b24fb7 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 @@ -455,4 +455,19 @@ public interface CollectionService public int countCollectionsWithSubmit(String q, Context context, Community community, String entityType) throws SQLException, SearchServiceException; + + /** + * Returns a list of all collections for a specific entity 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 entityType limit the returned collection to those related to given entity type + * @return list of collections found + * @throws SearchServiceException if search error + */ + public List findAllCollectionsByEntityType(Context context, String entityType) + throws SearchServiceException; } diff --git a/dspace/config/item-submission.dtd b/dspace/config/item-submission.dtd index 6490dac62c..dd1afa0dd0 100644 --- a/dspace/config/item-submission.dtd +++ b/dspace/config/item-submission.dtd @@ -11,7 +11,8 @@ diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index 2ab26dcf57..f937a5fd9a 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -47,6 +47,26 @@ --> + + + From 0409373b617a4f3c0b8e76c50133b14c6e016718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Sat, 20 May 2023 08:07:05 +0100 Subject: [PATCH 35/63] handling exceptions --- .../java/org/dspace/app/util/SubmissionConfigReader.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 7132c1e934..82ebbd0d0d 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 @@ -25,6 +25,7 @@ import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.CollectionService; import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.services.factory.DSpaceServicesFactory; import org.w3c.dom.Document; @@ -157,6 +158,9 @@ public class SubmissionConfigReader { } catch (FactoryConfigurationError fe) { throw new SubmissionConfigReaderException( "Cannot create Item Submission Configuration parser", fe); + } catch (SearchServiceException se) { + throw new SubmissionConfigReaderException( + "Cannot perform a discovery search for Item Submission Configuration", se); } catch (Exception e) { throw new SubmissionConfigReaderException( "Error creating Item Submission Configuration: " + e); @@ -292,7 +296,7 @@ public class SubmissionConfigReader { * should correspond to the collection-form maps, the form definitions, and * the display/storage word pairs. */ - private void doNodes(Node n) throws SAXException, SubmissionConfigReaderException { + private void doNodes(Node n) throws SAXException, SearchServiceException, SubmissionConfigReaderException { if (n == null) { return; } @@ -339,7 +343,7 @@ public class SubmissionConfigReader { * the collection handle and item submission name, put name in hashmap keyed * by the collection handle. */ - private void processMap(Node e) throws SAXException { + private void processMap(Node e) throws SAXException, SearchServiceException { // create a context Context context = new Context(); From 687b6216dfabb5e7d4069c30e7aeb2cecc73b602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Sat, 20 May 2023 10:41:51 +0100 Subject: [PATCH 36/63] checkstyle violations fixing --- .../java/org/dspace/app/util/SubmissionConfigReader.java | 6 +++++- .../main/java/org/dspace/content/CollectionServiceImpl.java | 1 - .../java/org/dspace/content/service/CollectionService.java | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) 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 82ebbd0d0d..91be9a08e6 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 @@ -107,7 +107,11 @@ public class SubmissionConfigReader { * always reload from scratch) */ private SubmissionConfig lastSubmissionConfig = null; - + + /** + * Collection Service instance, needed to interact with collection's + * stored data + */ protected static final CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService(); 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 ef89009ebf..2166a94738 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.Map; import java.util.MissingResourceException; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.UUID; 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 82d8b24fb7..9ded79fada 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 @@ -455,7 +455,6 @@ public interface CollectionService public int countCollectionsWithSubmit(String q, Context context, Community community, String entityType) throws SQLException, SearchServiceException; - /** * Returns a list of all collections for a specific entity type. * NOTE: for better performance, this method retrieves its results from an index (cache) From 6fa9e74d9006b260a7ca5edc40d734219b487682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Sat, 20 May 2023 11:35:27 +0100 Subject: [PATCH 37/63] checkstyle violations fixing --- .../main/java/org/dspace/app/util/SubmissionConfigReader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 91be9a08e6..d394c60e41 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 @@ -109,7 +109,7 @@ public class SubmissionConfigReader { private SubmissionConfig lastSubmissionConfig = null; /** - * Collection Service instance, needed to interact with collection's + * Collection Service instance, needed to interact with collection's * stored data */ protected static final CollectionService collectionService @@ -381,7 +381,7 @@ public class SubmissionConfigReader { List collections = collectionService.findAllCollectionsByEntityType( context, entityType); for (Collection collection : collections) { - collectionToSubmissionConfig.putIfAbsent(collection.getHandle(), value); + collectionToSubmissionConfig.putIfAbsent(collection.getHandle(), value); } } } // ignore any child node that isn't a "name-map" From 2ef268380fedec1aaba1acff291ab04e425eab84 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sat, 20 May 2023 12:56:38 +1200 Subject: [PATCH 38/63] Unlink DOI from item on deletion even if no provider is configured --- .../java/org/dspace/content/ItemServiceImpl.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 a290cb0d99..f86b6690ad 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -64,7 +64,9 @@ import org.dspace.eperson.service.SubscribeService; import org.dspace.event.Event; import org.dspace.harvest.HarvestedItem; import org.dspace.harvest.service.HarvestedItemService; +import org.dspace.identifier.DOI; import org.dspace.identifier.IdentifierException; +import org.dspace.identifier.service.DOIService; import org.dspace.identifier.service.IdentifierService; import org.dspace.orcid.OrcidHistory; import org.dspace.orcid.OrcidQueue; @@ -123,6 +125,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Autowired(required = true) protected IdentifierService identifierService; @Autowired(required = true) + protected DOIService doiService; + @Autowired(required = true) protected VersioningService versioningService; @Autowired(required = true) protected HarvestedItemService harvestedItemService; @@ -786,6 +790,16 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It // Remove any Handle handleService.unbindHandle(context, item); + // Delete a DOI if linked to the item. + // If no DOI consumer or provider is configured, but a DOI remains linked to this item's uuid, + // hibernate will throw a foreign constraint exception. + // Here we use the DOI service directly as it is able to manage DOIs even without any configured + // consumer or provider. + DOI doi = doiService.findDOIByDSpaceObject(context, item); + if (doi != null) { + doi.setDSpaceObject(null); + } + // remove version attached to the item removeVersion(context, item); From 208cac08d561de5992812fe2aaaf92929fedd4b4 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Sun, 21 May 2023 15:42:56 +1200 Subject: [PATCH 39/63] modifying unit tests as per CI feedback, stubbings now unnecessary --- .../src/test/java/org/dspace/content/CollectionTest.java | 3 --- dspace-api/src/test/java/org/dspace/content/ItemTest.java | 2 -- 2 files changed, 5 deletions(-) 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 1548ebcae0..13d037abf8 100644 --- a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java +++ b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java @@ -725,9 +725,6 @@ public class CollectionTest extends AbstractDSpaceObjectTest { // Allow Item REMOVE perms doNothing().when(authorizeServiceSpy) .authorizeAction(any(Context.class), any(Item.class), eq(Constants.REMOVE)); - // Allow Item WRITE perms (Needed to remove identifiers, e.g. DOI, before Item deletion) - doNothing().when(authorizeServiceSpy) - .authorizeAction(any(Context.class), any(Item.class), eq(Constants.WRITE)); // create & add item first context.turnOffAuthorisationSystem(); 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 15e425e23a..bae6ce9e1d 100644 --- a/dspace-api/src/test/java/org/dspace/content/ItemTest.java +++ b/dspace-api/src/test/java/org/dspace/content/ItemTest.java @@ -1189,8 +1189,6 @@ public class ItemTest extends AbstractDSpaceObjectTest { doNothing().when(authorizeServiceSpy).authorizeAction(context, item, Constants.REMOVE, true); // Allow Item DELETE perms doNothing().when(authorizeServiceSpy).authorizeAction(context, item, Constants.DELETE); - // Allow Item WRITE perms (required to first delete identifiers) - doNothing().when(authorizeServiceSpy).authorizeAction(context, item, Constants.WRITE); UUID id = item.getID(); itemService.delete(context, item); From 2b3af3a126ae9b5f523660cb544d7cd0a6192f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Mon, 22 May 2023 08:42:33 +0100 Subject: [PATCH 40/63] checkstyle violations fixing --- .../main/java/org/dspace/app/util/SubmissionConfigReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d394c60e41..9ed539ee4f 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 @@ -663,4 +663,4 @@ public class SubmissionConfigReader { } return results; } -} +} \ No newline at end of file From fc2589464f7e2471aff52b252c83fb4b6e7eebdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Mon, 22 May 2023 12:57:56 +0100 Subject: [PATCH 41/63] checkstyle violations fixing and remove unnecessary max rows limit --- .../main/java/org/dspace/app/util/SubmissionConfigReader.java | 2 +- .../src/main/java/org/dspace/content/CollectionServiceImpl.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 9ed539ee4f..0f144fd69f 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 @@ -381,7 +381,7 @@ public class SubmissionConfigReader { List collections = collectionService.findAllCollectionsByEntityType( context, entityType); for (Collection collection : collections) { - collectionToSubmissionConfig.putIfAbsent(collection.getHandle(), value); + collectionToSubmissionConfig.putIfAbsent(collection.getHandle(), value); } } } // ignore any child node that isn't a "name-map" 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 2166a94738..5b70cc4ec0 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -1054,7 +1054,6 @@ public class CollectionServiceImpl extends DSpaceObjectServiceImpl i List collectionList = new ArrayList<>(); DiscoverQuery discoverQuery = new DiscoverQuery(); - discoverQuery.setMaxResults(0); discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE); discoverQuery.addFilterQueries("dspace.entity.type:" + entityType); From 50f808a7d003b0e185e790ca501546481e9c4d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Tue, 23 May 2023 08:51:27 +0100 Subject: [PATCH 42/63] removing Person test configuration --- dspace/config/item-submission.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index f937a5fd9a..2f20e34c6b 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -66,7 +66,7 @@ --> - + From eb46a99dff6265aaf5d3e36a21cc2b8d8b3a7b6a Mon Sep 17 00:00:00 2001 From: Bui Thai Hai Date: Thu, 25 May 2023 09:57:13 +0700 Subject: [PATCH 43/63] Fix: default sort option (lastModified) for discovery --- .../DiscoverySortConfiguration.java | 14 ++++ .../discovery/utils/DiscoverQueryBuilder.java | 8 ++- .../org/dspace/discovery/DiscoveryIT.java | 68 +++++++++++++++++++ .../DiscoverConfigurationConverter.java | 9 +++ .../rest/model/SearchConfigurationRest.java | 10 +++ .../app/rest/DiscoveryRestControllerIT.java | 4 +- .../utils/RestDiscoverQueryBuilderTest.java | 15 ++++ dspace/config/spring/api/discovery.xml | 6 ++ dspace/solr/search/conf/schema.xml | 1 + 9 files changed, 132 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortConfiguration.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortConfiguration.java index e251d1bc51..cd1a4eecb8 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortConfiguration.java @@ -9,6 +9,7 @@ package org.dspace.discovery.configuration; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -22,6 +23,11 @@ public class DiscoverySortConfiguration { private List sortFields = new ArrayList(); + /** + * Default sort configuration to use when needed + */ + @Nullable private DiscoverySortFieldConfiguration defaultSortField; + public List getSortFields() { return sortFields; } @@ -30,6 +36,14 @@ public class DiscoverySortConfiguration { this.sortFields = sortFields; } + public DiscoverySortFieldConfiguration getDefaultSortField() { + return defaultSortField; + } + + public void setDefaultSortField(DiscoverySortFieldConfiguration configuration) { + this.defaultSortField = configuration; + } + public DiscoverySortFieldConfiguration getSortFieldConfiguration(String sortField) { if (StringUtils.isBlank(sortField)) { return null; 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 fa5cc32813..92a973dff8 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 @@ -332,7 +332,9 @@ public class DiscoverQueryBuilder implements InitializingBean { } private String getDefaultSortDirection(DiscoverySortConfiguration searchSortConfiguration, String sortOrder) { - if (Objects.nonNull(searchSortConfiguration.getSortFields()) && + if (searchSortConfiguration.getDefaultSortField() != null) { + sortOrder = searchSortConfiguration.getDefaultSortField().getDefaultSortOrder().name(); + } else if (Objects.nonNull(searchSortConfiguration.getSortFields()) && !searchSortConfiguration.getSortFields().isEmpty()) { sortOrder = searchSortConfiguration.getSortFields().get(0).getDefaultSortOrder().name(); } @@ -342,7 +344,9 @@ public class DiscoverQueryBuilder implements InitializingBean { private String getDefaultSortField(DiscoverySortConfiguration searchSortConfiguration) { String sortBy;// Attempt to find the default one, if none found we use SCORE sortBy = "score"; - if (Objects.nonNull(searchSortConfiguration.getSortFields()) && + if (searchSortConfiguration.getDefaultSortField() != null) { + sortBy = searchSortConfiguration.getDefaultSortField().getMetadataField(); + } else if (Objects.nonNull(searchSortConfiguration.getSortFields()) && !searchSortConfiguration.getSortFields().isEmpty()) { DiscoverySortFieldConfiguration defaultSort = searchSortConfiguration.getSortFields().get(0); if (StringUtils.isBlank(defaultSort.getMetadataField())) { diff --git a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java index 0d1cc13106..0c3a52ec79 100644 --- a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java +++ b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java @@ -8,13 +8,16 @@ package org.dspace.discovery; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import org.dspace.AbstractIntegrationTestWithDatabase; @@ -24,6 +27,7 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.builder.ClaimedTaskBuilder; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.builder.PoolTaskBuilder; import org.dspace.builder.WorkflowItemBuilder; @@ -39,6 +43,8 @@ import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.CollectionService; import org.dspace.content.service.ItemService; import org.dspace.content.service.WorkspaceItemService; +import org.dspace.discovery.configuration.DiscoveryConfiguration; +import org.dspace.discovery.configuration.DiscoverySortFieldConfiguration; import org.dspace.discovery.indexobject.IndexableClaimedTask; import org.dspace.discovery.indexobject.IndexableCollection; import org.dspace.discovery.indexobject.IndexableItem; @@ -731,6 +737,68 @@ public class DiscoveryIT extends AbstractIntegrationTestWithDatabase { } } + /** + * Test designed to check if default sort option for Discovery is working, using workspace + * DiscoveryConfiguration
+ * Note: this test will be skipped if workspace do not have a default sort option set and of + * metadataType dc_date_accessioned or lastModified + * @throws SearchServiceException + */ + @Test + public void searchWithDefaultSortServiceTest() throws SearchServiceException { + + DiscoveryConfiguration workspaceConf = SearchUtils.getDiscoveryConfiguration("workspace", null); + // Skip if no default sort option set for workspaceConf + if (workspaceConf.getSearchSortConfiguration().getDefaultSortField() == null) { + return; + } + + DiscoverySortFieldConfiguration defaultSortField = + workspaceConf.getSearchSortConfiguration().getDefaultSortField(); + + // Populate the testing objects: create items in eperson's workspace and perform search in it + int numberItems = 10; + context.turnOffAuthorisationSystem(); + EPerson submitter = EPersonBuilder.createEPerson(context).withEmail("submitter@example.org").build(); + context.setCurrentUser(submitter); + Community community = CommunityBuilder.createCommunity(context).build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + for (int i = 0; i < numberItems; i++) { + ItemBuilder.createItem(context, collection) + .withTitle("item " + i) + .build(); + } + + // Build query with default parameters (except for workspaceConf) + DiscoverQuery discoverQuery = SearchUtils.getQueryBuilder() + .buildQuery(context, new IndexableCollection(collection), workspaceConf,"",null,"Item",null,null, + null,null); + + DiscoverResult result = searchService.search(context, discoverQuery); + + if (defaultSortField.getMetadataField().equals("dc_date_accessioned")) { + // Verify that search results are sort by dc_date_accessioned + LinkedList dc_date_accesioneds = result.getIndexableObjects().stream() + .map(o -> ((Item) o.getIndexedObject()).getMetadata()) + .map(l -> l.stream().filter(m -> m.getMetadataField().toString().equals("dc_date_accessioned")) + .map(m -> m.getValue()).findFirst().orElse("") + ) + .collect(Collectors.toCollection(LinkedList::new)); + assertFalse(dc_date_accesioneds.isEmpty()); + for (int i = 1; i < dc_date_accesioneds.size() - 1; i++) { + assertTrue(dc_date_accesioneds.get(i).compareTo(dc_date_accesioneds.get(i + 1)) >= 0); + } + } else if (defaultSortField.getMetadataField().equals("lastModified")) { + LinkedList lastModifieds = result.getIndexableObjects().stream() + .map(o -> ((Item) o.getIndexedObject()).getLastModified().toString()) + .collect(Collectors.toCollection(LinkedList::new)); + assertFalse(lastModifieds.isEmpty()); + for (int i = 1; i < lastModifieds.size() - 1; i++) { + assertTrue(lastModifieds.get(i).compareTo(lastModifieds.get(i + 1)) >= 0); + } + } + } + private void assertSearchQuery(String resourceType, int size) throws SearchServiceException { assertSearchQuery(resourceType, size, size, 0, -1); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DiscoverConfigurationConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DiscoverConfigurationConverter.java index 73851bd945..41cf235a87 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DiscoverConfigurationConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/DiscoverConfigurationConverter.java @@ -80,6 +80,15 @@ public class DiscoverConfigurationConverter sortOption.setSortOrder(discoverySearchSortConfiguration.getDefaultSortOrder().name()); searchConfigurationRest.addSortOption(sortOption); } + + DiscoverySortFieldConfiguration defaultSortField = searchSortConfiguration.getDefaultSortField(); + if (defaultSortField != null) { + SearchConfigurationRest.SortOption sortOption = new SearchConfigurationRest.SortOption(); + sortOption.setName(defaultSortField.getMetadataField()); + sortOption.setActualName(defaultSortField.getType()); + sortOption.setSortOrder(defaultSortField.getDefaultSortOrder().name()); + searchConfigurationRest.setDefaultSortOption(sortOption); + } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SearchConfigurationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SearchConfigurationRest.java index 7ec1b22500..b25d827e75 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SearchConfigurationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SearchConfigurationRest.java @@ -31,6 +31,8 @@ public class SearchConfigurationRest extends BaseObjectRest { private List filters = new LinkedList<>(); private List sortOptions = new LinkedList<>(); + private SortOption defaultSortOption; + public String getCategory() { return CATEGORY; } @@ -75,6 +77,14 @@ public class SearchConfigurationRest extends BaseObjectRest { return sortOptions; } + public SortOption getDefaultSortOption() { + return defaultSortOption; + } + + public void setDefaultSortOption(SortOption defaultSortOption) { + this.defaultSortOption = defaultSortOption; + } + @Override public boolean equals(Object object) { return (object instanceof SearchConfigurationRest && 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 a115c8aa2f..dd0b2fe576 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 @@ -1286,8 +1286,10 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest context.restoreAuthSystemState(); + //Update this test since dc.date.accessioned is now configured for workspace configuration, + // which will return status 400 instead of 422 if left unchanged getClient().perform(get("/api/discover/search/objects") - .param("sort", "dc.date.accessioned, ASC") + .param("sort", "person.familyName, ASC") .param("configuration", "workspace")) .andExpect(status().isUnprocessableEntity()); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/RestDiscoverQueryBuilderTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/RestDiscoverQueryBuilderTest.java index 6c9544d2f9..511bb8f98b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/RestDiscoverQueryBuilderTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/RestDiscoverQueryBuilderTest.java @@ -115,6 +115,8 @@ public class RestDiscoverQueryBuilderTest { sortConfiguration.setSortFields(listSortField); + sortConfiguration.setDefaultSortField(defaultSort); + discoveryConfiguration.setSearchSortConfiguration(sortConfiguration); DiscoverySearchFilterFacet subjectFacet = new DiscoverySearchFilterFacet(); @@ -167,6 +169,19 @@ public class RestDiscoverQueryBuilderTest { page.getOffset(), "SCORE", "ASC"); } + @Test + public void testSortByDefaultSortField() throws Exception { + page = PageRequest.of(2, 10, Sort.Direction.DESC, "dc.date.accessioned"); + restQueryBuilder.buildQuery(context, null, discoveryConfiguration, null, null, emptyList(), page); + + verify(discoverQueryBuilder, times(1)) + .buildQuery(context, null, discoveryConfiguration, null, emptyList(), emptyList(), + page.getPageSize(), page.getOffset(), + discoveryConfiguration.getSearchSortConfiguration().getDefaultSortField().getMetadataField(), + discoveryConfiguration.getSearchSortConfiguration().getDefaultSortField() + .getDefaultSortOrder().name().toUpperCase()); + } + @Test(expected = DSpaceBadRequestException.class) public void testCatchIllegalArgumentException() throws Exception { when(discoverQueryBuilder.buildQuery(any(), any(), any(), any(), any(), anyList(), any(), any(), any(), diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 611e77b27b..3f0f507451 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -865,8 +865,11 @@ + + + @@ -938,6 +941,8 @@ + + @@ -1015,6 +1020,7 @@ + diff --git a/dspace/solr/search/conf/schema.xml b/dspace/solr/search/conf/schema.xml index caa646ba1b..df21afbc64 100644 --- a/dspace/solr/search/conf/schema.xml +++ b/dspace/solr/search/conf/schema.xml @@ -283,6 +283,7 @@ + From b3a21ebd5a81701899753d83511746ab90f36cce Mon Sep 17 00:00:00 2001 From: Bui Thai Hai Date: Thu, 25 May 2023 14:54:46 +0700 Subject: [PATCH 44/63] Minor Tweaks --- .../java/org/dspace/app/rest/DiscoveryRestControllerIT.java | 4 +--- dspace/config/spring/api/discovery.xml | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) 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 dd0b2fe576..a115c8aa2f 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 @@ -1286,10 +1286,8 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest context.restoreAuthSystemState(); - //Update this test since dc.date.accessioned is now configured for workspace configuration, - // which will return status 400 instead of 422 if left unchanged getClient().perform(get("/api/discover/search/objects") - .param("sort", "person.familyName, ASC") + .param("sort", "dc.date.accessioned, ASC") .param("configuration", "workspace")) .andExpect(status().isUnprocessableEntity()); } diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 3f0f507451..45e5829e1a 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -866,10 +866,10 @@ - + - + From f3b939e88f63fd83f56dd1ea7f29781ce398d4fe Mon Sep 17 00:00:00 2001 From: Yana De Pauw Date: Thu, 25 May 2023 15:34:05 +0300 Subject: [PATCH 45/63] 94299: Add rest.patch.operations.limit to config file --- .../dspace/app/rest/repository/BitstreamRestRepository.java | 2 +- .../java/org/dspace/app/rest/BitstreamRestRepositoryIT.java | 4 ++-- dspace/config/modules/rest.cfg | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java index 454b6f8453..12e27dccac 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java @@ -265,7 +265,7 @@ public class BitstreamRestRepository extends DSpaceObjectRestRepository ops = new ArrayList<>(); RemoveOperation removeOp1 = new RemoveOperation(OPERATION_PATH_BITSTREAM_REMOVE + bitstream1.getID()); ops.add(removeOp1); @@ -2577,7 +2577,7 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest String token = getAuthToken(admin.getEmail(), password); Assert.assertTrue(bitstreamExists(token, bitstream1, bitstream2, bitstream3, bitstream4)); - DSpaceServicesFactory.getInstance().getConfigurationService().setProperty("patch.operations.limit", 2); + DSpaceServicesFactory.getInstance().getConfigurationService().setProperty("rest.patch.operations.limit", 2); getClient(token).perform(patch("/api/core/bitstreams") .content(patchBody) diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 6421258c57..657e02b58d 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -25,6 +25,10 @@ rest.projections.full.max = 2 # This property determines the max embed depth for a SpecificLevelProjection rest.projection.specificLevel.maxEmbed = 5 +# This property determines the max amount of rest operations that can be performed at the same time, for example when +# batch removing bitstreams. The default value is set to 1000. +rest.patch.operations.limit = 1000 + # Define which configuration properties are exposed through the http:///api/config/properties/ # rest endpoint. If a rest request is made for a property which exists, but isn't listed here, the server will # respond that the property wasn't found. This property can be defined multiple times to allow access to multiple From 4fa51d03d11259cd67506247be406d5b7faa7e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Fri, 26 May 2023 17:14:10 +0100 Subject: [PATCH 46/63] adding support for access status xoai plugin --- .../AccessStatusElementItemCompilePlugin.java | 67 ++++++++++++++++ .../oai/metadataFormats/oai_openaire.xsl | 78 +++++++++++++++++-- dspace/config/spring/oai/oai.xml | 4 + 3 files changed, 144 insertions(+), 5 deletions(-) create mode 100644 dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java b/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java new file mode 100644 index 0000000000..65ec251b21 --- /dev/null +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.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.xoai.app.plugins; + +import java.sql.SQLException; +import java.util.List; +import org.dspace.access.status.factory.AccessStatusServiceFactory; +import org.dspace.access.status.service.AccessStatusService; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.xoai.app.XOAIExtensionItemCompilePlugin; +import org.dspace.xoai.util.ItemUtils; +import com.lyncode.xoai.dataprovider.xml.xoai.Element; +import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; + +/** + * AccessStatusElementItemCompilePlugin aims to add structured information about the + * Access Status of the item (if any). + + * The xoai document will be enriched with a structure like that + *
+ * {@code
+ *   
+ *       
+ *          open.access
+ *       ;
+ *   ;
+ * }
+ * 
+ * Returning Values are based on: + * @see org.dspace.access.status.DefaultAccessStatusHelper DefaultAccessStatusHelper + */ +public class AccessStatusElementItemCompilePlugin implements XOAIExtensionItemCompilePlugin { + + @Override + public Metadata additionalMetadata(Context context, Metadata metadata, Item item) { + AccessStatusService accessStatusService = AccessStatusServiceFactory.getInstance().getAccessStatusService(); + + try { + String accessStatusType; + accessStatusType = accessStatusService.getAccessStatus(context, item); + + Element accessStatus = ItemUtils.create("access-status"); + accessStatus.getField().add(ItemUtils.createValue("value", accessStatusType)); + + Element others; + List elements = metadata.getElement(); + if (ItemUtils.getElement(elements, "others") != null) { + others = ItemUtils.getElement(elements, "others"); + } else { + others = ItemUtils.create("others"); + } + others.getElement().add(accessStatus); + + } catch (SQLException e) { + e.printStackTrace(); + } + + return metadata; + } + +} diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 7b66eaf043..19b1486f4c 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -93,6 +93,9 @@ + + @@ -658,6 +661,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1207,7 +1244,7 @@ - + + + + + + + + + + + open access + + + embargoed access + + + restricted access + + + metadata only access + + + + + + + + From e889abc6238b257bbde537814e011a842f4512d1 Mon Sep 17 00:00:00 2001 From: nwoodward Date: Fri, 26 May 2023 14:21:57 -0500 Subject: [PATCH 47/63] check that zip file exists and has correct MIME type; also make sure that common temp imports directory is not removed --- .../org/dspace/app/itemimport/ItemImport.java | 49 ++++++++++++++++-- .../dspace/app/itemimport/ItemImportCLI.java | 23 +++++++- .../app/itemimport/ItemImportCLIIT.java | 24 +++++++++ .../org/dspace/app/itemimport/test.pdf | Bin 0 -> 56812 bytes .../dspace/app/itemimport/ItemImportIT.java | 6 +++ 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 dspace-api/src/test/resources/org/dspace/app/itemimport/test.pdf diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java index ac9db76051..bcf7afed38 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java @@ -23,6 +23,7 @@ import java.util.UUID; import org.apache.commons.cli.ParseException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.tika.Tika; import org.dspace.app.itemimport.factory.ItemImportServiceFactory; import org.dspace.app.itemimport.service.ItemImportService; import org.dspace.authorize.AuthorizeException; @@ -77,6 +78,7 @@ public class ItemImport extends DSpaceRunnable { protected boolean zip = false; protected boolean remoteUrl = false; protected String zipfilename = null; + protected boolean zipvalid= false; protected boolean help = false; protected File workDir = null; protected File workFile = null; @@ -235,11 +237,19 @@ public class ItemImport extends DSpaceRunnable { handler.logInfo("***End of Test Run***"); } } finally { - // clean work dir if (zip) { - FileUtils.deleteDirectory(new File(sourcedir)); - FileUtils.deleteDirectory(workDir); - if (remoteUrl && workFile != null && workFile.exists()) { + // if zip file was valid then clean sourcedir + if (zipvalid && sourcedir != null && new File(sourcedir).exists()) { + FileUtils.deleteDirectory(new File(sourcedir)); + } + + // clean workdir + if (workDir != null && workDir.exists()) { + FileUtils.deleteDirectory(workDir); + } + + // conditionally clean workFile if import was done in the UI or via a URL and it still exists + if (workFile != null && workFile.exists()) { workFile.delete(); } } @@ -329,7 +339,14 @@ public class ItemImport extends DSpaceRunnable { // manage zip via remote url optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); } + if (optionalFileStream.isPresent()) { + // validate zip file + Optional validationFileStream = handler.getFileStream(context, zipfilename); + if (validationFileStream.isPresent()) { + validateZip(validationFileStream.get()); + } + workFile = new File(itemImportService.getTempWorkDir() + File.separator + zipfilename + "-" + context.getCurrentUser().getID()); FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile); @@ -337,10 +354,32 @@ public class ItemImport extends DSpaceRunnable { throw new IllegalArgumentException( "Error reading file, the file couldn't be found for filename: " + zipfilename); } - workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR); + + workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR + + File.separator + context.getCurrentUser().getID()); sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath()); } + /** + * Confirm that the zip file has the correct MIME type + * @param inputStream + */ + protected void validateZip(InputStream inputStream) { + Tika tika = new Tika(); + try { + String mimeType = tika.detect(inputStream); + if (mimeType.equals("application/zip")) { + zipvalid = true; + } else { + handler.logError("A valid zip file must be supplied. The provided file has mimetype: " + mimeType); + throw new UnsupportedOperationException("A valid zip file must be supplied"); + } + } catch (IOException e) { + throw new IllegalArgumentException( + "There was an error while reading the zip file: " + zipfilename); + } + } + /** * Read the mapfile * @param context diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java index 1a71a8c4c0..98d2469b71 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImportCLI.java @@ -8,6 +8,7 @@ package org.dspace.app.itemimport; import java.io.File; +import java.io.FileInputStream; import java.io.InputStream; import java.net.URL; import java.sql.SQLException; @@ -101,6 +102,17 @@ public class ItemImportCLI extends ItemImport { // If this is a zip archive, unzip it first if (zip) { if (!remoteUrl) { + // confirm zip file exists + File myZipFile = new File(sourcedir + File.separator + zipfilename); + if ((!myZipFile.exists()) || (!myZipFile.isFile())) { + throw new IllegalArgumentException( + "Error reading file, the file couldn't be found for filename: " + zipfilename); + } + + // validate zip file + InputStream validationFileStream = new FileInputStream(myZipFile); + validateZip(validationFileStream); + workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR + File.separator + context.getCurrentUser().getID()); sourcedir = itemImportService.unzip( @@ -109,15 +121,22 @@ public class ItemImportCLI extends ItemImport { // manage zip via remote url Optional optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); if (optionalFileStream.isPresent()) { + // validate zip file via url + Optional validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); + if (validationFileStream.isPresent()) { + validateZip(validationFileStream.get()); + } + workFile = new File(itemImportService.getTempWorkDir() + File.separator + zipfilename + "-" + context.getCurrentUser().getID()); FileUtils.copyInputStreamToFile(optionalFileStream.get(), workFile); + workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR + + File.separator + context.getCurrentUser().getID()); + sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath()); } else { throw new IllegalArgumentException( "Error reading file, the file couldn't be found for filename: " + zipfilename); } - workDir = new File(itemImportService.getTempWorkDir() + File.separator + TEMP_DIR); - sourcedir = itemImportService.unzip(workFile, workDir.getAbsolutePath()); } } } diff --git a/dspace-api/src/test/java/org/dspace/app/itemimport/ItemImportCLIIT.java b/dspace-api/src/test/java/org/dspace/app/itemimport/ItemImportCLIIT.java index 411e8de4df..02a0a8aee0 100644 --- a/dspace-api/src/test/java/org/dspace/app/itemimport/ItemImportCLIIT.java +++ b/dspace-api/src/test/java/org/dspace/app/itemimport/ItemImportCLIIT.java @@ -8,6 +8,7 @@ package org.dspace.app.itemimport; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.io.File; import java.nio.file.Files; @@ -33,6 +34,7 @@ import org.dspace.content.service.ItemService; import org.dspace.content.service.RelationshipService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.flywaydb.core.internal.util.ExceptionUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -46,6 +48,7 @@ import org.junit.Test; public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase { private static final String ZIP_NAME = "saf.zip"; + private static final String PDF_NAME = "test.pdf"; private static final String publicationTitle = "A Tale of Two Cities"; private static final String personTitle = "Person Test"; @@ -55,6 +58,7 @@ public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase { private Collection collection; private Path tempDir; private Path workDir; + private static final String TEMP_DIR = ItemImport.TEMP_DIR; @Before @Override @@ -226,6 +230,10 @@ public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase { checkMetadata(); checkMetadataWithAnotherSchema(); checkBitstream(); + + // confirm that TEMP_DIR still exists + File workTempDir = new File(workDir + File.separator + TEMP_DIR); + assertTrue(workTempDir.exists()); } @Test @@ -254,6 +262,22 @@ public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase { checkRelationship(); } + @Test + public void importItemByZipSafInvalidMimetype() throws Exception { + // use sample PDF file + Files.copy(getClass().getResourceAsStream("test.pdf"), + Path.of(tempDir.toString() + "/" + PDF_NAME)); + + String[] args = new String[] { "import", "-a", "-e", admin.getEmail(), "-c", collection.getID().toString(), + "-s", tempDir.toString(), "-z", PDF_NAME, "-m", tempDir.toString() + "/mapfile.out" }; + try { + perfomImportScript(args); + } catch (Exception e) { + // should throw an exception due to invalid mimetype + assertEquals(UnsupportedOperationException.class, ExceptionUtils.getRootCause(e).getClass()); + } + } + @Test public void resumeImportItemBySafWithMetadataOnly() throws Exception { // create simple SAF diff --git a/dspace-api/src/test/resources/org/dspace/app/itemimport/test.pdf b/dspace-api/src/test/resources/org/dspace/app/itemimport/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5b3749cbff73a41baf8aa06b7f62339886bc6ca2 GIT binary patch literal 56812 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%0AGv`t_CiE@W~}d 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 Date: Fri, 26 May 2023 15:01:43 -0500 Subject: [PATCH 48/63] fix checkstyle --- .../src/main/java/org/dspace/app/itemimport/ItemImport.java | 2 +- .../test/java/org/dspace/app/itemimport/ItemImportCLIIT.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java index bcf7afed38..c94e163243 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java @@ -78,7 +78,7 @@ public class ItemImport extends DSpaceRunnable { protected boolean zip = false; protected boolean remoteUrl = false; protected String zipfilename = null; - protected boolean zipvalid= false; + protected boolean zipvalid = false; protected boolean help = false; protected File workDir = null; protected File workFile = null; diff --git a/dspace-api/src/test/java/org/dspace/app/itemimport/ItemImportCLIIT.java b/dspace-api/src/test/java/org/dspace/app/itemimport/ItemImportCLIIT.java index 02a0a8aee0..08ae3af4ae 100644 --- a/dspace-api/src/test/java/org/dspace/app/itemimport/ItemImportCLIIT.java +++ b/dspace-api/src/test/java/org/dspace/app/itemimport/ItemImportCLIIT.java @@ -269,7 +269,8 @@ public class ItemImportCLIIT extends AbstractIntegrationTestWithDatabase { Path.of(tempDir.toString() + "/" + PDF_NAME)); String[] args = new String[] { "import", "-a", "-e", admin.getEmail(), "-c", collection.getID().toString(), - "-s", tempDir.toString(), "-z", PDF_NAME, "-m", tempDir.toString() + "/mapfile.out" }; + "-s", tempDir.toString(), "-z", PDF_NAME, "-m", tempDir.toString() + + "/mapfile.out" }; try { perfomImportScript(args); } catch (Exception e) { From 43ab705568b136651d9086119a11001c73cc08e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Fri, 26 May 2023 22:47:02 +0100 Subject: [PATCH 49/63] fixing code style errors --- .../plugins/AccessStatusElementItemCompilePlugin.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java b/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java index 65ec251b21..a854b4b42d 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java @@ -9,18 +9,19 @@ package org.dspace.xoai.app.plugins; import java.sql.SQLException; import java.util.List; + +import com.lyncode.xoai.dataprovider.xml.xoai.Element; +import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; import org.dspace.access.status.factory.AccessStatusServiceFactory; import org.dspace.access.status.service.AccessStatusService; import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.xoai.app.XOAIExtensionItemCompilePlugin; import org.dspace.xoai.util.ItemUtils; -import com.lyncode.xoai.dataprovider.xml.xoai.Element; -import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; /** * AccessStatusElementItemCompilePlugin aims to add structured information about the - * Access Status of the item (if any). + * Access Status of the item (if any). * The xoai document will be enriched with a structure like that *
@@ -32,7 +33,7 @@ import com.lyncode.xoai.dataprovider.xml.xoai.Metadata;
  *   ;
  * }
  * 
- * Returning Values are based on: + * Returning Values are based on: * @see org.dspace.access.status.DefaultAccessStatusHelper DefaultAccessStatusHelper */ public class AccessStatusElementItemCompilePlugin implements XOAIExtensionItemCompilePlugin { From a9eab4a254b6fa151da0a86dfdf02217afdcf965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Sat, 27 May 2023 08:23:46 +0100 Subject: [PATCH 50/63] also add support for access status at bitstream level --- .../crosswalks/oai/metadataFormats/oai_openaire.xsl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 19b1486f4c..3a1d75eb56 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -75,6 +75,9 @@ + + @@ -93,9 +96,6 @@ - - @@ -1162,11 +1162,11 @@ - + + select="/doc:metadata/doc:element[@name='others']/doc:element[@name='access-status']/doc:field[@name='value']/text()"/> From c11679c6defd3d68496006fdbb3c01869ef5a3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20Gra=C3=A7a?= Date: Sat, 27 May 2023 09:19:15 +0100 Subject: [PATCH 51/63] removing tailing semicolon --- .../app/plugins/AccessStatusElementItemCompilePlugin.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java b/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java index a854b4b42d..6b3c5ded98 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/plugins/AccessStatusElementItemCompilePlugin.java @@ -29,8 +29,8 @@ import org.dspace.xoai.util.ItemUtils; * * * open.access - * ; - * ; + *
+ * * } * * Returning Values are based on: From b272b1fcab1f5340c35563c5840dadd01d4a7d33 Mon Sep 17 00:00:00 2001 From: "Mark H. Wood" Date: Wed, 31 May 2023 17:37:34 -0400 Subject: [PATCH 52/63] Make service lookup retry log at DEBUG; radically shorten the trace. --- .../servicemanager/DSpaceServiceManager.java | 19 +++++--- .../java/org/dspace/utils/CallStackUtils.java | 44 +++++++++++++++++++ 2 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 dspace-services/src/main/java/org/dspace/utils/CallStackUtils.java diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java index afd1627f5e..6cffa7ee66 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java @@ -7,6 +7,8 @@ */ package org.dspace.servicemanager; +import static org.apache.logging.log4j.Level.DEBUG; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; @@ -21,6 +23,8 @@ import java.util.Map; import javax.annotation.PreDestroy; import org.apache.commons.lang3.ArrayUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.kernel.Activator; import org.dspace.kernel.config.SpringLoader; import org.dspace.kernel.mixins.ConfigChangeListener; @@ -28,8 +32,7 @@ import org.dspace.kernel.mixins.ServiceChangeListener; import org.dspace.kernel.mixins.ServiceManagerReadyAware; import org.dspace.servicemanager.config.DSpaceConfigurationService; import org.dspace.servicemanager.spring.DSpaceBeanFactoryPostProcessor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.dspace.utils.CallStackUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -44,7 +47,7 @@ import org.springframework.context.support.ClassPathXmlApplicationContext; */ public final class DSpaceServiceManager implements ServiceManagerSystem { - private static Logger log = LoggerFactory.getLogger(DSpaceServiceManager.class); + private static Logger log = LogManager.getLogger(); public static final String CONFIG_PATH = "spring/spring-dspace-applicationContext.xml"; public static final String CORE_RESOURCE_PATH = "classpath*:spring/spring-dspace-core-services.xml"; @@ -426,9 +429,10 @@ public final class DSpaceServiceManager implements ServiceManagerSystem { service = (T) applicationContext.getBean(name, type); } catch (BeansException e) { // no luck, try the fall back option - log.warn( + log.debug( "Unable to locate bean by name or id={}." - + " Will try to look up bean by type next.", name, e); + + " Will try to look up bean by type next.", name); + CallStackUtils.logCaller(log, DEBUG); service = null; } } else { @@ -437,8 +441,9 @@ public final class DSpaceServiceManager implements ServiceManagerSystem { service = (T) applicationContext.getBean(type.getName(), type); } catch (BeansException e) { // no luck, try the fall back option - log.warn("Unable to locate bean by name or id={}." - + " Will try to look up bean by type next.", type.getName(), e); + log.debug("Unable to locate bean by name or id={}." + + " Will try to look up bean by type next.", type::getName); + CallStackUtils.logCaller(log, DEBUG); service = null; } } diff --git a/dspace-services/src/main/java/org/dspace/utils/CallStackUtils.java b/dspace-services/src/main/java/org/dspace/utils/CallStackUtils.java new file mode 100644 index 0000000000..cb60a223a1 --- /dev/null +++ b/dspace-services/src/main/java/org/dspace/utils/CallStackUtils.java @@ -0,0 +1,44 @@ +/** + * 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.utils; + +import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; + +import java.lang.StackWalker.StackFrame; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Logger; + +/** + * Utility methods for manipulating call stacks. + * + * @author mwood + */ +public class CallStackUtils { + private CallStackUtils() {} + + /** + * Log the class, method and line of the caller's caller. + * + * @param log logger to use. + * @param level log at this level, if enabled. + */ + static public void logCaller(Logger log, Level level) { + if (log.isEnabled(level)) { + StackWalker stack = StackWalker.getInstance(RETAIN_CLASS_REFERENCE); + StackFrame caller = stack.walk(stream -> stream.skip(2) + .findFirst() + .get()); + String callerClassName = caller.getDeclaringClass().getCanonicalName(); + String callerMethodName = caller.getMethodName(); + int callerLine = caller.getLineNumber(); + log.log(level, "Called from {}.{} line {}.", + callerClassName, callerMethodName, callerLine); + } + } +} From 0ec27875bcb3bab66a38ddc3987ed401c67a2f0d Mon Sep 17 00:00:00 2001 From: Francesco Pio Scognamiglio Date: Wed, 31 May 2023 16:47:02 +0200 Subject: [PATCH 53/63] [DURACOM-149] use right formatter for mapping of dc.date.issued in pubmed live import; added integration tests --- .../PubmedDateMetadatumContributor.java | 18 +- .../PubmedImportMetadataSourceServiceIT.java | 213 ++++++++++++++++++ .../app/rest/pubmedimport-fetch-test.xml | 14 ++ .../app/rest/pubmedimport-fetch-test2.xml | 14 ++ .../app/rest/pubmedimport-search-test.xml | 194 ++++++++++++++++ .../app/rest/pubmedimport-search-test2.xml | 132 +++++++++++ 6 files changed, 578 insertions(+), 7 deletions(-) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedImportMetadataSourceServiceIT.java create mode 100644 dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-fetch-test.xml create mode 100644 dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-fetch-test2.xml create mode 100644 dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test.xml create mode 100644 dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test2.xml diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedDateMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedDateMetadatumContributor.java index 6536026058..add9caef1b 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedDateMetadatumContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmed/metadatamapping/contributor/PubmedDateMetadatumContributor.java @@ -15,8 +15,8 @@ import java.util.Date; import java.util.LinkedList; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; -import org.dspace.content.DCDate; import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; import org.dspace.importer.external.metadatamapping.MetadataFieldMapping; import org.dspace.importer.external.metadatamapping.MetadatumDTO; @@ -107,28 +107,30 @@ public class PubmedDateMetadatumContributor implements MetadataContributor LinkedList dayList = (LinkedList) day.contributeMetadata(t); for (int i = 0; i < yearList.size(); i++) { - DCDate dcDate = null; + String resultDateString = ""; String dateString = ""; + SimpleDateFormat resultFormatter = null; if (monthList.size() > i && dayList.size() > i) { dateString = yearList.get(i).getValue() + "-" + monthList.get(i).getValue() + "-" + dayList.get(i).getValue(); + resultFormatter = new SimpleDateFormat("yyyy-MM-dd"); } else if (monthList.size() > i) { dateString = yearList.get(i).getValue() + "-" + monthList.get(i).getValue(); + resultFormatter = new SimpleDateFormat("yyyy-MM"); } else { dateString = yearList.get(i).getValue(); + resultFormatter = new SimpleDateFormat("yyyy"); } int j = 0; // Use the first dcDate that has been formatted (Config should go from most specific to most lenient) - while (j < dateFormatsToAttempt.size()) { + while (j < dateFormatsToAttempt.size() && StringUtils.isBlank(resultDateString)) { String dateFormat = dateFormatsToAttempt.get(j); try { SimpleDateFormat formatter = new SimpleDateFormat(dateFormat); Date date = formatter.parse(dateString); - dcDate = new DCDate(date); - values.add(metadataFieldMapping.toDCValue(field, formatter.format(date))); - break; + resultDateString = resultFormatter.format(date); } catch (ParseException e) { // Multiple dateformats can be configured, we don't want to print the entire stacktrace every // time one of those formats fails. @@ -138,7 +140,9 @@ public class PubmedDateMetadatumContributor implements MetadataContributor } j++; } - if (dcDate == null) { + if (StringUtils.isNotBlank(resultDateString)) { + values.add(metadataFieldMapping.toDCValue(field, resultDateString)); + } else { log.info( "Failed parsing " + dateString + ", check " + "the configured dataformats in config/spring/api/pubmed-integration.xml"); 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 new file mode 100644 index 0000000000..79b8ec3f72 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/PubmedImportMetadataSourceServiceIT.java @@ -0,0 +1,213 @@ +/** + * 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; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.liveimportclient.service.LiveImportClientImpl; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.pubmed.service.PubmedImportMetadataSourceServiceImpl; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link PubmedImportMetadataSourceServiceImpl} + * + * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com) + */ +public class PubmedImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private PubmedImportMetadataSourceServiceImpl pubmedImportMetadataServiceImpl; + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Test + public void pubmedImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + try (InputStream fetchFile = getClass().getResourceAsStream("pubmedimport-fetch-test.xml"); + InputStream searchFile = getClass().getResourceAsStream("pubmedimport-search-test.xml")) { + liveImportClientImpl.setHttpClient(httpClient); + + CloseableHttpResponse fetchResponse = mockResponse( + IOUtils.toString(fetchFile, Charset.defaultCharset()), 200, "OK"); + CloseableHttpResponse searchResponse = mockResponse( + IOUtils.toString(searchFile, Charset.defaultCharset()), 200, "OK"); + + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(fetchResponse).thenReturn(searchResponse); + + context.restoreAuthSystemState(); + ArrayList collection2match = getRecords(); + Collection recordsImported = pubmedImportMetadataServiceImpl.getRecords("test query", 0, 1); + assertEquals(1, recordsImported.size()); + matchRecords(new ArrayList(recordsImported), collection2match); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test + public void pubmedImportMetadataGetRecords2Test() throws Exception { + context.turnOffAuthorisationSystem(); + + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + try (InputStream fetchFile = getClass().getResourceAsStream("pubmedimport-fetch-test2.xml"); + InputStream searchFile = getClass().getResourceAsStream("pubmedimport-search-test2.xml")) { + liveImportClientImpl.setHttpClient(httpClient); + + CloseableHttpResponse fetchResponse = mockResponse( + IOUtils.toString(fetchFile, Charset.defaultCharset()), 200, "OK"); + CloseableHttpResponse searchResponse = mockResponse( + IOUtils.toString(searchFile, Charset.defaultCharset()), 200, "OK"); + + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(fetchResponse).thenReturn(searchResponse); + + context.restoreAuthSystemState(); + ArrayList collection2match = getRecords2(); + Collection recordsImported = pubmedImportMetadataServiceImpl.getRecords("test query", 0, 1); + assertEquals(1, recordsImported.size()); + matchRecords(new ArrayList(recordsImported), collection2match); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + private ArrayList getRecords() { + ArrayList records = new ArrayList<>(); + List metadatums = new ArrayList(); + //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 identifierOther = createMetadatumDTO("dc", "identifier", "other", "36708638"); + MetadatumDTO author1 = createMetadatumDTO("dc", "contributor", "author", "Giuffrida, Silvia"); + MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "Silano, Verdiana"); + MetadatumDTO author3 = createMetadatumDTO("dc", "contributor", "author", "Ramacciati, Nicola"); + MetadatumDTO author4 = createMetadatumDTO("dc", "contributor", "author", "Prandi, Cesarina"); + MetadatumDTO author5 = createMetadatumDTO("dc", "contributor", "author", "Baldon, Alessia"); + MetadatumDTO author6 = createMetadatumDTO("dc", "contributor", "author", "Bianchi, Monica"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2023-02"); + MetadatumDTO language = createMetadatumDTO("dc", "language", "iso", "en"); + MetadatumDTO subject1 = createMetadatumDTO("dc", "subject", null, "Advanced practice nursing"); + MetadatumDTO subject2 = createMetadatumDTO("dc", "subject", null, "Clinical reasoning"); + MetadatumDTO subject3 = createMetadatumDTO("dc", "subject", null, "Critical thinking"); + MetadatumDTO subject4 = createMetadatumDTO("dc", "subject", null, "Educational strategies"); + MetadatumDTO subject5 = createMetadatumDTO("dc", "subject", null, "Nursing education"); + MetadatumDTO subject6 = createMetadatumDTO("dc", "subject", null, "Teaching methodology"); + + 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); + metadatums.add(author3); + metadatums.add(author4); + metadatums.add(author5); + metadatums.add(author6); + metadatums.add(date); + metadatums.add(language); + metadatums.add(subject1); + metadatums.add(subject2); + metadatums.add(subject3); + metadatums.add(subject4); + metadatums.add(subject5); + metadatums.add(subject6); + ImportRecord record = new ImportRecord(metadatums); + + records.add(record); + return records; + } + + private ArrayList getRecords2() { + ArrayList records = new ArrayList<>(); + List metadatums = new ArrayList(); + //define first record + MetadatumDTO title = createMetadatumDTO("dc","title", null, "Searching NCBI Databases Using Entrez."); + MetadatumDTO description = createMetadatumDTO("dc", "description", "abstract", "One of the most widely" + + " used interfaces for the retrieval of information from biological databases is the NCBI Entrez" + + " system. Entrez capitalizes on the fact that there are pre-existing, logical relationships between" + + " the individual entries found in numerous public databases. The existence of such natural" + + " connections, mostly biological in nature, argued for the development of a method through which" + + " all the information about a particular biological entity could be found without having to" + + " sequentially visit and query disparate databases. Two basic protocols describe simple, text-based" + + " searches, illustrating the types of information that can be retrieved through the Entrez system." + + " An alternate protocol builds upon the first basic protocol, using additional," + + " built-in features of the Entrez system, and providing alternative ways to issue the initial query." + + " The support protocol reviews how to save frequently issued queries. Finally, Cn3D, a structure" + + " visualization tool, is also discussed."); + MetadatumDTO identifierOther = createMetadatumDTO("dc", "identifier", "other", "21975942"); + MetadatumDTO author1 = createMetadatumDTO("dc", "contributor", "author", "Gibney, Gretchen"); + MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "Baxevanis, Andreas D"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2011-10"); + MetadatumDTO language = createMetadatumDTO("dc", "language", "iso", "en"); + + metadatums.add(title); + metadatums.add(description); + metadatums.add(identifierOther); + metadatums.add(author1); + metadatums.add(author2); + metadatums.add(date); + metadatums.add(language); + ImportRecord record = new ImportRecord(metadatums); + + records.add(record); + return records; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-fetch-test.xml b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-fetch-test.xml new file mode 100644 index 0000000000..4f921658e3 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-fetch-test.xml @@ -0,0 +1,14 @@ + + + + 1 + 1 + 0 + 1 + MCID_64784b5ab65e3b2b2253cd3a + + 36708638 + + + "10 1016 j nepr 2023 103548"[All Fields] + \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-fetch-test2.xml b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-fetch-test2.xml new file mode 100644 index 0000000000..1ff9570777 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-fetch-test2.xml @@ -0,0 +1,14 @@ + + + + 1 + 1 + 0 + 1 + MCID_64784b12ccf058150336d6a8 + + 21975942 + + + "10 1002 0471142905 hg0610s71"[All Fields] + \ 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 new file mode 100644 index 0000000000..666fb1e7d5 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test.xml @@ -0,0 +1,194 @@ + + + + + + 36708638 + + 2023 + 02 + 23 + + + 2023 + 02 + 23 + +
+ + 1873-5223 + + 67 + + 2023 + Feb + + + Nurse education in practice + Nurse Educ Pract + + Teaching strategies of clinical reasoning in advanced nursing clinical practice: A scoping review. + + 103548 + 103548 + + 10.1016/j.nepr.2023.103548 + S1471-5953(23)00010-0 + + 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. + 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. + 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. + 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. + Copyright © 2023 Elsevier Ltd. All rights reserved. + + + + Giuffrida + Silvia + S + + Department of Cardiology and Cardiac Surgery, Cardio Centro Ticino Institute, Ente Ospedaliero Cantonale, Lugano, Switzerland. Electronic address: silvia.giuffrida@eoc.ch. + + + + Silano + Verdiana + V + + Nursing Direction of Settore Anziani Città di Bellinzona, Bellinzona, Switzerland. Electronic address: verdiana.silano@hotmail.it. + + + + Ramacciati + Nicola + N + + Department of Pharmacy, Health and Nutritional Sciences (DFSSN), University of Calabria, Rende, Italy. Electronic address: nicola.ramacciati@unical.it. + + + + Prandi + Cesarina + C + + Department of Business Economics, Health and Social Care (DEASS), University of Applied Sciences and Arts of Southern Switzerland, Manno, Switzerland. Electronic address: cesarina.prandi@supsi.ch. + + + + Baldon + Alessia + A + + Department of Business Economics, Health and Social Care (DEASS), University of Applied Sciences and Arts of Southern Switzerland, Manno, Switzerland. Electronic address: alessia.baldon@supsi.ch. + + + + Bianchi + Monica + M + + Department of Business Economics, Health and Social Care (DEASS), University of Applied Sciences and Arts of Southern Switzerland, Manno, Switzerland. Electronic address: monica.bianchi@supsi.ch. + + + + eng + + Journal Article + Review + + + 2023 + 01 + 17 + +
+ + Scotland + Nurse Educ Pract + 101090848 + 1471-5953 + + IM + + + Humans + + + Advanced Practice Nursing + + + Learning + + + Curriculum + + + Thinking + + + Clinical Reasoning + + + Students, Nursing + + + + Advanced practice nursing + Clinical reasoning + Critical thinking + Educational strategies + Nursing education + Teaching methodology + + Declaration of Competing Interest The authors declare that they have no known competing financial interests or personal relationships that could have appeared to influence the work reported in this paper. +
+ + + + 2022 + 11 + 9 + + + 2022 + 12 + 17 + + + 2023 + 1 + 10 + + + 2023 + 1 + 29 + 6 + 0 + + + 2023 + 2 + 25 + 6 + 0 + + + 2023 + 1 + 28 + 18 + 7 + + + ppublish + + 36708638 + 10.1016/j.nepr.2023.103548 + S1471-5953(23)00010-0 + + +
+
\ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test2.xml b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test2.xml new file mode 100644 index 0000000000..949d3b1250 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/pubmedimport-search-test2.xml @@ -0,0 +1,132 @@ + + + + + + 21975942 + + 2012 + 01 + 13 + + + 2016 + 10 + 21 + +
+ + 1934-8258 + + Chapter 6 + + 2011 + Oct + + + Current protocols in human genetics + Curr Protoc Hum Genet + + Searching NCBI Databases Using Entrez. + + Unit6.10 + Unit6.10 + + 10.1002/0471142905.hg0610s71 + + One of the most widely used interfaces for the retrieval of information from biological databases is the NCBI Entrez system. Entrez capitalizes on the fact that there are pre-existing, logical relationships between the individual entries found in numerous public databases. The existence of such natural connections, mostly biological in nature, argued for the development of a method through which all the information about a particular biological entity could be found without having to sequentially visit and query disparate databases. Two basic protocols describe simple, text-based searches, illustrating the types of information that can be retrieved through the Entrez system. An alternate protocol builds upon the first basic protocol, using additional, built-in features of the Entrez system, and providing alternative ways to issue the initial query. The support protocol reviews how to save frequently issued queries. Finally, Cn3D, a structure visualization tool, is also discussed. + © 2011 by John Wiley & Sons, Inc. + + + + Gibney + Gretchen + G + + + Baxevanis + Andreas D + AD + + + eng + + Journal Article + +
+ + United States + Curr Protoc Hum Genet + 101287858 + 1934-8258 + + IM + + + Animals + + + Database Management Systems + + + Databases, Factual + + + Humans + + + Information Storage and Retrieval + methods + + + Internet + + + Molecular Conformation + + + National Library of Medicine (U.S.) + + + PubMed + + + United States + + + User-Computer Interface + + +
+ + + + 2011 + 10 + 7 + 6 + 0 + + + 2011 + 10 + 7 + 6 + 0 + + + 2012 + 1 + 14 + 6 + 0 + + + ppublish + + 21975942 + 10.1002/0471142905.hg0610s71 + + +
+
\ No newline at end of file From eb224eb8096fa9d7173cace2a2b8625d40b807eb Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Sun, 14 May 2023 21:52:31 +0200 Subject: [PATCH 54/63] 100553: Added stricter metadata field & schema validation --- .../MetadataFieldRestRepository.java | 12 ++-- .../MetadataSchemaRestRepository.java | 6 +- .../rest/MetadataSchemaRestRepositoryIT.java | 16 ++++- .../rest/MetadatafieldRestRepositoryIT.java | 60 +++++++++++++++++-- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java index eefd6331d1..5152f11902 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java @@ -260,14 +260,18 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository Date: Fri, 2 Jun 2023 09:45:52 +0700 Subject: [PATCH 55/63] Tweaks fo test cases. --- .../org/dspace/discovery/DiscoveryIT.java | 36 +++++++++---------- .../utils/RestDiscoverQueryBuilderTest.java | 7 ++-- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java index 0c3a52ec79..164525afb1 100644 --- a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java +++ b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java @@ -768,6 +768,7 @@ public class DiscoveryIT extends AbstractIntegrationTestWithDatabase { .withTitle("item " + i) .build(); } + context.restoreAuthSystemState(); // Build query with default parameters (except for workspaceConf) DiscoverQuery discoverQuery = SearchUtils.getQueryBuilder() @@ -776,26 +777,21 @@ public class DiscoveryIT extends AbstractIntegrationTestWithDatabase { DiscoverResult result = searchService.search(context, discoverQuery); - if (defaultSortField.getMetadataField().equals("dc_date_accessioned")) { - // Verify that search results are sort by dc_date_accessioned - LinkedList dc_date_accesioneds = result.getIndexableObjects().stream() - .map(o -> ((Item) o.getIndexedObject()).getMetadata()) - .map(l -> l.stream().filter(m -> m.getMetadataField().toString().equals("dc_date_accessioned")) - .map(m -> m.getValue()).findFirst().orElse("") - ) - .collect(Collectors.toCollection(LinkedList::new)); - assertFalse(dc_date_accesioneds.isEmpty()); - for (int i = 1; i < dc_date_accesioneds.size() - 1; i++) { - assertTrue(dc_date_accesioneds.get(i).compareTo(dc_date_accesioneds.get(i + 1)) >= 0); - } - } else if (defaultSortField.getMetadataField().equals("lastModified")) { - LinkedList lastModifieds = result.getIndexableObjects().stream() - .map(o -> ((Item) o.getIndexedObject()).getLastModified().toString()) - .collect(Collectors.toCollection(LinkedList::new)); - assertFalse(lastModifieds.isEmpty()); - for (int i = 1; i < lastModifieds.size() - 1; i++) { - assertTrue(lastModifieds.get(i).compareTo(lastModifieds.get(i + 1)) >= 0); - } + /* + // code example for testing against sort by dc_date_accessioned + LinkedList dc_date_accesioneds = result.getIndexableObjects().stream() + .map(o -> ((Item) o.getIndexedObject()).getMetadata()) + .map(l -> l.stream().filter(m -> m.getMetadataField().toString().equals("dc_date_accessioned")) + .map(m -> m.getValue()).findFirst().orElse("") + ) + .collect(Collectors.toCollection(LinkedList::new)); + }*/ + LinkedList lastModifieds = result.getIndexableObjects().stream() + .map(o -> ((Item) o.getIndexedObject()).getLastModified().toString()) + .collect(Collectors.toCollection(LinkedList::new)); + assertFalse(lastModifieds.isEmpty()); + for (int i = 1; i < lastModifieds.size() - 1; i++) { + assertTrue(lastModifieds.get(i).compareTo(lastModifieds.get(i + 1)) >= 0); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/RestDiscoverQueryBuilderTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/RestDiscoverQueryBuilderTest.java index 511bb8f98b..e21f395f09 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/RestDiscoverQueryBuilderTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/RestDiscoverQueryBuilderTest.java @@ -171,15 +171,12 @@ public class RestDiscoverQueryBuilderTest { @Test public void testSortByDefaultSortField() throws Exception { - page = PageRequest.of(2, 10, Sort.Direction.DESC, "dc.date.accessioned"); + page = PageRequest.of(2, 10); restQueryBuilder.buildQuery(context, null, discoveryConfiguration, null, null, emptyList(), page); verify(discoverQueryBuilder, times(1)) .buildQuery(context, null, discoveryConfiguration, null, emptyList(), emptyList(), - page.getPageSize(), page.getOffset(), - discoveryConfiguration.getSearchSortConfiguration().getDefaultSortField().getMetadataField(), - discoveryConfiguration.getSearchSortConfiguration().getDefaultSortField() - .getDefaultSortOrder().name().toUpperCase()); + page.getPageSize(), page.getOffset(), null, null); } @Test(expected = DSpaceBadRequestException.class) From 7c85b007c027c959217b3b9bc5d730006223d448 Mon Sep 17 00:00:00 2001 From: Francesco Pio Scognamiglio Date: Fri, 2 Jun 2023 10:10:45 +0200 Subject: [PATCH 56/63] [DURACOM-149] remove trailing whitespace --- .../dspace/app/rest/PubmedImportMetadataSourceServiceIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 79b8ec3f72..3b39d25121 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 @@ -30,7 +30,7 @@ import org.springframework.beans.factory.annotation.Autowired; /** * Integration tests for {@link PubmedImportMetadataSourceServiceImpl} - * + * * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com) */ public class PubmedImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { From a38ff421694a5be590a41928985e3f8cd54c1b37 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Thu, 25 May 2023 14:52:11 +0300 Subject: [PATCH 57/63] dspace: capture publisher from CrossRef live import Publisher is a required field on CrossRef so we can always rely on capturing this information when doing a live import. See: https://github.com/CrossRef/rest-api-doc/blob/master/api_format.md --- .../app/rest/CrossRefImportMetadataSourceServiceIT.java | 8 +++++++- dspace/config/spring/api/crossref-integration.xml | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) 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 11fe58ac1d..72524709ec 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 @@ -153,6 +153,8 @@ public class CrossRefImportMetadataSourceServiceIT extends AbstractLiveImportInt MetadatumDTO issn = createMetadatumDTO("dc", "identifier", "issn", "2415-3060"); MetadatumDTO volume = createMetadatumDTO("oaire", "citation", "volume", "1"); MetadatumDTO issue = createMetadatumDTO("oaire", "citation", "issue", "2"); + MetadatumDTO publisher = createMetadatumDTO("dc", "publisher", null, + "Petro Mohyla Black Sea National University"); metadatums.add(title); metadatums.add(author); @@ -163,6 +165,7 @@ public class CrossRefImportMetadataSourceServiceIT extends AbstractLiveImportInt metadatums.add(issn); metadatums.add(volume); metadatums.add(issue); + metadatums.add(publisher); ImportRecord firstrRecord = new ImportRecord(metadatums); @@ -179,6 +182,8 @@ public class CrossRefImportMetadataSourceServiceIT extends AbstractLiveImportInt MetadatumDTO issn2 = createMetadatumDTO("dc", "identifier", "issn", "2415-3060"); MetadatumDTO volume2 = createMetadatumDTO("oaire", "citation", "volume", "1"); MetadatumDTO issue2 = createMetadatumDTO("oaire", "citation", "issue", "2"); + MetadatumDTO publisher2 = createMetadatumDTO("dc", "publisher", null, + "Petro Mohyla Black Sea National University"); metadatums2.add(title2); metadatums2.add(author2); @@ -189,6 +194,7 @@ public class CrossRefImportMetadataSourceServiceIT extends AbstractLiveImportInt metadatums2.add(issn2); metadatums2.add(volume2); metadatums2.add(issue2); + metadatums2.add(publisher2); ImportRecord secondRecord = new ImportRecord(metadatums2); records.add(firstrRecord); @@ -196,4 +202,4 @@ public class CrossRefImportMetadataSourceServiceIT extends AbstractLiveImportInt return records; } -} \ 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 5d67c17626..d1e416d2b0 100644 --- a/dspace/config/spring/api/crossref-integration.xml +++ b/dspace/config/spring/api/crossref-integration.xml @@ -30,6 +30,7 @@ + @@ -137,6 +138,14 @@ + + + + + + + + From a0a1844de7c6aa4fdc5a743b402e1c056c57ace5 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Mon, 5 Jun 2023 16:01:31 -0500 Subject: [PATCH 58/63] Fix test class compilation --- .../src/test/java/org/dspace/discovery/DiscoveryIT.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java index 164525afb1..55be531418 100644 --- a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java +++ b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java @@ -7,6 +7,7 @@ */ package org.dspace.discovery; +import static org.dspace.discovery.SolrServiceWorkspaceWorkflowRestrictionPlugin.DISCOVER_WORKSPACE_CONFIGURATION_NAME; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -746,8 +747,8 @@ public class DiscoveryIT extends AbstractIntegrationTestWithDatabase { */ @Test public void searchWithDefaultSortServiceTest() throws SearchServiceException { - - DiscoveryConfiguration workspaceConf = SearchUtils.getDiscoveryConfiguration("workspace", null); + DiscoveryConfiguration workspaceConf = + SearchUtils.getDiscoveryConfiguration(context, DISCOVER_WORKSPACE_CONFIGURATION_NAME, null); // Skip if no default sort option set for workspaceConf if (workspaceConf.getSearchSortConfiguration().getDefaultSortField() == null) { return; From 14bb32036c2195802a7db7d790e5994244618062 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Tue, 6 Jun 2023 16:01:04 +0200 Subject: [PATCH 59/63] DURACOM-136 add explaination of the commandLineParameters in the javadoc --- .../org/dspace/scripts/configuration/ScriptConfiguration.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/scripts/configuration/ScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/scripts/configuration/ScriptConfiguration.java index e22063eb49..642409a924 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/configuration/ScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/scripts/configuration/ScriptConfiguration.java @@ -87,6 +87,8 @@ public abstract class ScriptConfiguration implements B * to the currentUser in the context being an admin, however this can be overwritten by each script individually * if different rules apply * @param context The relevant DSpace context + * @param commandLineParameters the parameters that will be used to start the process if known, + * null otherwise * @return A boolean indicating whether the script is allowed to execute or not */ public boolean isAllowedToExecute(Context context, List commandLineParameters) { From 2b523ba5ac1cbaeaf8bccd9f5b575e7564e14aae Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Tue, 6 Jun 2023 16:01:47 +0200 Subject: [PATCH 60/63] DURACOM-136 improve handling and testing of invalid mimetype --- .../app/rest/ScriptProcessesController.java | 18 ++++++++++++++++-- .../app/rest/ScriptRestRepositoryIT.java | 14 +++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ScriptProcessesController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ScriptProcessesController.java index 3aeec9535b..70149bbb6b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ScriptProcessesController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ScriptProcessesController.java @@ -19,9 +19,12 @@ import org.dspace.app.rest.model.hateoas.ProcessResource; import org.dspace.app.rest.repository.ScriptRestRepository; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.core.Context; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.dspace.scripts.service.ScriptService; import org.dspace.services.RequestService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.ControllerUtils; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.hateoas.RepresentationModel; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -50,6 +53,9 @@ public class ScriptProcessesController { @Autowired private ScriptRestRepository scriptRestRepository; + @Autowired + private ScriptService scriptService; + @Autowired private RequestService requestService; @@ -80,9 +86,17 @@ public class ScriptProcessesController { @RequestMapping(method = RequestMethod.POST, consumes = "!" + MediaType.MULTIPART_FORM_DATA_VALUE) @PreAuthorize("hasAuthority('AUTHENTICATED')") public ResponseEntity> startProcessInvalidMimeType( - @PathVariable(name = "name") String scriptName, - @RequestParam(name = "file", required = false) List files) + @PathVariable(name = "name") String scriptName) throws Exception { + if (log.isTraceEnabled()) { + log.trace("Starting Process for Script with name: " + scriptName); + } + Context context = ContextUtil.obtainContext(requestService.getCurrentRequest().getHttpServletRequest()); + ScriptConfiguration scriptToExecute = scriptService.getScriptConfiguration(scriptName); + + if (scriptToExecute == null) { + throw new ResourceNotFoundException("The script for name: " + scriptName + " wasn't found"); + } throw new DSpaceBadRequestException("Invalid mimetype"); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java index 16e691ef6f..29a0076d0c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java @@ -134,6 +134,13 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(jsonPath("$.page.totalElements", is(0))); } + @Test + public void findAllScriptsAnonymousUserTest() throws Exception { + getClient().perform(get("/api/system/scripts")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + @Test public void findAllScriptsLocalAdminsTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -543,8 +550,13 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { public void postProcessAdminWithWrongContentTypeBadRequestException() throws Exception { String token = getAuthToken(admin.getEmail(), password); + + getClient(token) + .perform(post("/api/system/scripts/mock-script/processes")) + .andExpect(status().isBadRequest()); + getClient(token).perform(post("/api/system/scripts/mock-script-invalid/processes")) - .andExpect(status().isBadRequest()); + .andExpect(status().isNotFound()); } @Test From 32cd24b7538694ec964289bb13027d09ae06b829 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Tue, 6 Jun 2023 19:45:31 +0200 Subject: [PATCH 61/63] DURACOM-136 restrict script endpoints to authenticated users, add test to proof that standard script are reseved to site administrator --- .../rest/repository/ScriptRestRepository.java | 4 +- .../app/rest/ScriptRestRepositoryIT.java | 42 +++++++++++++++++-- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java index 2fc996a327..09d65590b6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java @@ -59,7 +59,7 @@ public class ScriptRestRepository extends DSpaceRestRepository findAll(Context context, Pageable pageable) { List scriptConfigurations = scriptService.getScriptConfigurations(context); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java index 29a0076d0c..42c9f2c9f7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java @@ -136,9 +136,9 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void findAllScriptsAnonymousUserTest() throws Exception { + // this should be changed once we allow anonymous user to execute some scripts getClient().perform(get("/api/system/scripts")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", is(0))); + .andExpect(status().isUnauthorized()); } @Test @@ -358,12 +358,48 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isNotFound()); } + /** + * This test will create a basic structure of communities, collections and items with some local admins at each + * level and verify that the local admins, nor generic users can run scripts reserved to administrator + * (i.e. default one that don't override the default + * {@link ScriptConfiguration#isAllowedToExecute(org.dspace.core.Context, List)} method implementation + */ @Test public void postProcessNonAdminAuthorizeException() throws Exception { - String token = getAuthToken(eperson.getEmail(), password); + context.turnOffAuthorisationSystem(); + EPerson comAdmin = EPersonBuilder.createEPerson(context) + .withEmail("comAdmin@example.com") + .withPassword(password).build(); + EPerson colAdmin = EPersonBuilder.createEPerson(context) + .withEmail("colAdmin@example.com") + .withPassword(password).build(); + EPerson itemAdmin = EPersonBuilder.createEPerson(context) + .withEmail("itemAdmin@example.com") + .withPassword(password).build(); + Community community = CommunityBuilder.createCommunity(context) + .withName("Community") + .withAdminGroup(comAdmin) + .build(); + Collection collection = CollectionBuilder.createCollection(context, community) + .withName("Collection") + .withAdminGroup(colAdmin) + .build(); + Item item = ItemBuilder.createItem(context, collection).withAdminUser(itemAdmin) + .withTitle("Test item to curate").build(); + context.restoreAuthSystemState(); + String token = getAuthToken(eperson.getEmail(), password); + String comAdmin_token = getAuthToken(eperson.getEmail(), password); + String colAdmin_token = getAuthToken(eperson.getEmail(), password); + String itemAdmin_token = getAuthToken(eperson.getEmail(), password); getClient(token).perform(multipart("/api/system/scripts/mock-script/processes")) .andExpect(status().isForbidden()); + getClient(comAdmin_token).perform(multipart("/api/system/scripts/mock-script/processes")) + .andExpect(status().isForbidden()); + getClient(colAdmin_token).perform(multipart("/api/system/scripts/mock-script/processes")) + .andExpect(status().isForbidden()); + getClient(itemAdmin_token).perform(multipart("/api/system/scripts/mock-script/processes")) + .andExpect(status().isForbidden()); } @Test From 5a8c7a397c342c5f931b209395965bbc365dfa3f Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Tue, 6 Jun 2023 20:12:23 +0200 Subject: [PATCH 62/63] DURACOM-136 open endpoints to retrieve files of process to the user that has triggered the process --- .../ProcessFileTypesLinkRepository.java | 2 +- .../ProcessFilesLinkRepository.java | 2 +- .../ProcessOutputLinkRepository.java | 2 +- .../app/rest/ProcessRestRepositoryIT.java | 81 ++++++++++++++----- 4 files changed, 66 insertions(+), 21 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessFileTypesLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessFileTypesLinkRepository.java index 8eb8d7ef65..16c8115b29 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessFileTypesLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessFileTypesLinkRepository.java @@ -47,7 +47,7 @@ public class ProcessFileTypesLinkRepository extends AbstractDSpaceRestRepository * @throws SQLException If something goes wrong * @throws AuthorizeException If something goes wrong */ - @PreAuthorize("hasAuthority('ADMIN')") + @PreAuthorize("hasPermission(#processId, 'PROCESS', 'READ')") public ProcessFileTypesRest getFileTypesFromProcess(@Nullable HttpServletRequest request, Integer processId, @Nullable Pageable optionalPageable, diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessFilesLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessFilesLinkRepository.java index 42fcef0d62..5d8251cf19 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessFilesLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessFilesLinkRepository.java @@ -47,7 +47,7 @@ public class ProcessFilesLinkRepository extends AbstractDSpaceRestRepository imp * @throws SQLException If something goes wrong * @throws AuthorizeException If something goes wrong */ - @PreAuthorize("hasAuthority('ADMIN')") + @PreAuthorize("hasPermission(#processId, 'PROCESS', 'READ')") public Page getFilesFromProcess(@Nullable HttpServletRequest request, Integer processId, @Nullable Pageable optionalPageable, diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessOutputLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessOutputLinkRepository.java index f9f665d14f..f5b3edced2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessOutputLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ProcessOutputLinkRepository.java @@ -50,7 +50,7 @@ public class ProcessOutputLinkRepository extends AbstractDSpaceRestRepository im * @throws SQLException If something goes wrong * @throws AuthorizeException If something goes wrong */ - @PreAuthorize("hasAuthority('ADMIN')") + @PreAuthorize("hasPermission(#processId, 'PROCESS', 'READ')") public BitstreamRest getOutputFromProcess(@Nullable HttpServletRequest request, Integer processId, @Nullable Pageable optionalPageable, diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java index d76e20b23d..670d8e2f35 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ProcessRestRepositoryIT.java @@ -222,22 +222,35 @@ public class ProcessRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void getProcessFiles() throws Exception { + context.setCurrentUser(eperson); Process newProcess = ProcessBuilder.createProcess(context, eperson, "mock-script", new LinkedList<>()).build(); - try (InputStream is = IOUtils.toInputStream("Test File For Process", CharEncoding.UTF_8)) { - processService.appendFile(context, process, is, "inputfile", "test.csv"); + processService.appendFile(context, newProcess, is, "inputfile", "test.csv"); } - Bitstream bitstream = processService.getBitstream(context, process, "inputfile"); + Bitstream bitstream = processService.getBitstream(context, newProcess, "inputfile"); String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(get("/api/system/processes/" + process.getID() + "/files")) + getClient(token).perform(get("/api/system/processes/" + newProcess.getID() + "/files")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.files[0].name", is("test.csv"))) .andExpect(jsonPath("$._embedded.files[0].uuid", is(bitstream.getID().toString()))) .andExpect(jsonPath("$._embedded.files[0].metadata['dspace.process.filetype']" + "[0].value", is("inputfile"))); - + getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); + // also the user that triggered the process should be able to access the process' files + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken) + .perform(get("/api/system/processes/" + newProcess.getID() + "/files")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.files[0].name", is("test.csv"))) + .andExpect(jsonPath("$._embedded.files[0].uuid", is(bitstream.getID().toString()))) + .andExpect(jsonPath("$._embedded.files[0].metadata['dspace.process.filetype']" + + "[0].value", is("inputfile"))); + getClient(epersonToken) + .perform(get("/api/core/bitstreams/" + bitstream.getID() + "/content")) + .andExpect(status().isOk()); } @Test @@ -245,25 +258,34 @@ public class ProcessRestRepositoryIT extends AbstractControllerIntegrationTest { Process newProcess = ProcessBuilder.createProcess(context, eperson, "mock-script", new LinkedList<>()).build(); try (InputStream is = IOUtils.toInputStream("Test File For Process", CharEncoding.UTF_8)) { - processService.appendFile(context, process, is, "inputfile", "test.csv"); + processService.appendFile(context, newProcess, is, "inputfile", "test.csv"); } - Bitstream bitstream = processService.getBitstream(context, process, "inputfile"); + Bitstream bitstream = processService.getBitstream(context, newProcess, "inputfile"); String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(get("/api/system/processes/" + process.getID() + "/files/inputfile")) + getClient(token).perform(get("/api/system/processes/" + newProcess.getID() + "/files/inputfile")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.bitstreams[0].name", is("test.csv"))) + .andExpect(jsonPath("$._embedded.bitstreams[0].uuid", is(bitstream.getID().toString()))) + .andExpect(jsonPath("$._embedded.bitstreams[0].metadata['dspace.process.filetype']" + + "[0].value", is("inputfile"))); + // also the user that triggered the process should be able to access the process' files + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken) + .perform(get("/api/system/processes/" + newProcess.getID() + "/files/inputfile")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.bitstreams[0].name", is("test.csv"))) .andExpect(jsonPath("$._embedded.bitstreams[0].uuid", is(bitstream.getID().toString()))) .andExpect(jsonPath("$._embedded.bitstreams[0].metadata['dspace.process.filetype']" + "[0].value", is("inputfile"))); - } @Test public void getProcessFilesTypes() throws Exception { + Process newProcess = ProcessBuilder.createProcess(context, eperson, "mock-script", new LinkedList<>()).build(); try (InputStream is = IOUtils.toInputStream("Test File For Process", CharEncoding.UTF_8)) { - processService.appendFile(context, process, is, "inputfile", "test.csv"); + processService.appendFile(context, newProcess, is, "inputfile", "test.csv"); } List fileTypesToCheck = new LinkedList<>(); @@ -271,12 +293,18 @@ public class ProcessRestRepositoryIT extends AbstractControllerIntegrationTest { String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(get("/api/system/processes/" + process.getID() + "/filetypes")) + getClient(token).perform(get("/api/system/processes/" + newProcess.getID() + "/filetypes")) .andExpect(status().isOk()) .andExpect(jsonPath("$", ProcessFileTypesMatcher - .matchProcessFileTypes("filetypes-" + process.getID(), fileTypesToCheck))); - + .matchProcessFileTypes("filetypes-" + newProcess.getID(), fileTypesToCheck))); + // also the user that triggered the process should be able to access the process' files + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken) + .perform(get("/api/system/processes/" + newProcess.getID() + "/filetypes")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", ProcessFileTypesMatcher + .matchProcessFileTypes("filetypes-" + newProcess.getID(), fileTypesToCheck))); } @Test @@ -811,25 +839,42 @@ public class ProcessRestRepositoryIT extends AbstractControllerIntegrationTest { @Test public void getProcessOutput() throws Exception { + context.setCurrentUser(eperson); + Process process1 = ProcessBuilder.createProcess(context, eperson, "mock-script", parameters) + .withStartAndEndTime("10/01/1990", "20/01/1990") + .build(); + try (InputStream is = IOUtils.toInputStream("Test File For Process", CharEncoding.UTF_8)) { - processService.appendLog(process.getID(), process.getName(), "testlog", ProcessLogLevel.INFO); + processService.appendLog(process1.getID(), process1.getName(), "testlog", ProcessLogLevel.INFO); } - processService.createLogBitstream(context, process); + processService.createLogBitstream(context, process1); List fileTypesToCheck = new LinkedList<>(); fileTypesToCheck.add("inputfile"); String token = getAuthToken(admin.getEmail(), password); - getClient(token).perform(get("/api/system/processes/" + process.getID() + "/output")) + getClient(token).perform(get("/api/system/processes/" + process1.getID() + "/output")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name", - is(process.getName() + process.getID() + ".log"))) + is(process1.getName() + process1.getID() + ".log"))) .andExpect(jsonPath("$.type", is("bitstream"))) .andExpect(jsonPath("$.metadata['dc.title'][0].value", - is(process.getName() + process.getID() + ".log"))) + is(process1.getName() + process1.getID() + ".log"))) .andExpect(jsonPath("$.metadata['dspace.process.filetype'][0].value", is("script_output"))); + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken) + .perform(get("/api/system/processes/" + process1.getID() + "/output")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.name", + is(process1.getName() + process1.getID() + ".log"))) + .andExpect(jsonPath("$.type", is("bitstream"))) + .andExpect(jsonPath("$.metadata['dc.title'][0].value", + is(process1.getName() + process1.getID() + ".log"))) + .andExpect(jsonPath("$.metadata['dspace.process.filetype'][0].value", + is("script_output"))); } } From 7757c4e898373fc06b03e070eca67bd58d2ad4cf Mon Sep 17 00:00:00 2001 From: Francesco Pio Scognamiglio Date: Thu, 8 Jun 2023 13:50:11 +0200 Subject: [PATCH 63/63] [DURACOM-153] fix validation to use the retrieved zip file on saf import --- .../src/main/java/org/dspace/app/itemimport/ItemImport.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java index c94e163243..b32de11f7a 100644 --- a/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java +++ b/dspace-api/src/main/java/org/dspace/app/itemimport/ItemImport.java @@ -332,17 +332,19 @@ public class ItemImport extends DSpaceRunnable { */ protected void readZip(Context context, ItemImportService itemImportService) throws Exception { Optional optionalFileStream = Optional.empty(); + Optional validationFileStream = Optional.empty(); if (!remoteUrl) { // manage zip via upload optionalFileStream = handler.getFileStream(context, zipfilename); + validationFileStream = handler.getFileStream(context, zipfilename); } else { // manage zip via remote url optionalFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); + validationFileStream = Optional.ofNullable(new URL(zipfilename).openStream()); } - if (optionalFileStream.isPresent()) { + if (validationFileStream.isPresent()) { // validate zip file - Optional validationFileStream = handler.getFileStream(context, zipfilename); if (validationFileStream.isPresent()) { validateZip(validationFileStream.get()); }