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 ae3cf91d4c40..beb390ca6445 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 @@ -7,12 +7,22 @@ */ package org.dspace.app.rest.repository; +import static org.apache.commons.lang3.ArrayUtils.nullToEmpty; + import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.util.HashMap; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Spliterators; import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; @@ -32,11 +42,14 @@ import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; +import org.dspace.content.MetadataValue; import org.dspace.content.service.BitstreamService; import org.dspace.content.service.BundleService; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; +import org.dspace.content.service.ItemService; import org.dspace.core.Context; +import org.dspace.core.exception.SQLRuntimeException; import org.dspace.handle.service.HandleService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -72,6 +85,9 @@ public class BitstreamRestRepository extends DSpaceObjectRestRepository findByItemId(@Parameter(value = "uuid", required = true) UUID uuid, + @Parameter(value = "name", required = true) String bundleName, + @Parameter(value = "filterMetadata") String[] filterMetadataFields, + @Parameter(value = "filterMetadataValue") String[] filterMetadataValues, + Pageable pageable) { + + Map filterMetadata = composeFilterMetadata(filterMetadataFields, filterMetadataValues); + + Item item = findItemById(uuid) + .orElseThrow(() -> new UnprocessableEntityException("No item found with the given UUID")); + + List bitstreams = getItemBitstreams(item) + .filter(bitstream -> isContainedInBundleNamed(bitstream, bundleName)) + .filter(bitstream -> hasAllMetadataValues(bitstream, filterMetadata)) + .collect(Collectors.toList()); + + return converter.toRestPage(bitstreams, pageable, utils.obtainProjection()); + } + private Bitstream getFirstMatchedBitstream(Item item, Integer sequence, String filename) { List bundles = item.getBundles(); List bitstreams = new LinkedList<>(); @@ -248,4 +295,74 @@ public BundleRest performBitstreamMove(Context context, Bitstream bitstream, Bun return converter.toRest(targetBundle, utils.obtainProjection()); } + + private Optional findItemById(UUID uuid) { + try { + return Optional.ofNullable(itemService.find(obtainContext(), uuid)); + } catch (SQLException e) { + throw new SQLRuntimeException(e); + } + } + + private Map composeFilterMetadata(String[] fields, String[] values) { + + if (filterMetadataDoNotHaveSameCardinality(fields, values)) { + throw new IllegalArgumentException("The request must include a filterMetadata " + + "and a filterMetadataValue parameters with the same cardinality"); + } + + Map filterMetadata = new HashMap(); + + for (int i = 0; i < nullToEmpty(fields).length; i++) { + filterMetadata.put(fields[i], values[i]); + } + + return filterMetadata; + + } + + private boolean filterMetadataDoNotHaveSameCardinality(String[] fields, String[] values) { + return nullToEmpty(fields).length != nullToEmpty(values).length; + } + + private Stream getItemBitstreams(Item item) { + try { + Iterator bitstreamIterator = bs.getItemBitstreams(obtainContext(), item); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(bitstreamIterator, 0), false); + } catch (SQLException e) { + throw new SQLRuntimeException(e); + } + } + + private boolean isContainedInBundleNamed(Bitstream bitstream, String name) { + try { + return bitstream.getBundles().stream() + .anyMatch(bundle -> bundle.getName().equals(name)); + } catch (SQLException e) { + throw new SQLRuntimeException(e); + } + } + + private boolean hasAllMetadataValues(Bitstream bitstream, Map filterMetadata) { + return filterMetadata.keySet().stream() + .allMatch(metadataField -> hasMetadataValue(bitstream, metadataField, filterMetadata.get(metadataField))); + } + + private boolean hasMetadataValue(Bitstream bitstream, String metadataField, String value) { + return bitstream.getMetadata().stream() + .filter(metadataValue -> metadataValue.getMetadataField().toString('.').equals(metadataField)) + .anyMatch(metadataValue -> matchesMetadataValue(metadataValue, value)); + } + + private boolean matchesMetadataValue(MetadataValue metadataValue, String value) { + + if (value.startsWith("(") && value.endsWith(")")) { + value = value.substring(1, value.length() - 1); + return metadataValue.getValue().matches(value); + } else { + return metadataValue.getValue().equals(value); + } + + } + } 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 f9c1e469fcfe..8f0d74ddde46 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 @@ -11,6 +11,8 @@ import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataDoesNotExist; import static org.dspace.core.Constants.WRITE; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -2279,6 +2281,356 @@ public void findByHandleAndFileNameForPublicItemWithEmbargoOnFile() throws Excep )); } + @Test + public void findByItemIdWithoutRequiredParameters() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .build(); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString())) + .andExpect(status().isBadRequest()); + + } + + @Test + public void findByItemIdWithMetadataFieldsAndValuesWithDifferentCardinality() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .build(); + + context.restoreAuthSystemState(); + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", "bundle name") + .param("filterMetadata", "dc.title")) + .andExpect(status().isBadRequest()); + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", "bundle name") + .param("filterMetadata", "dc.title") + .param("filterMetadataValue", "Test", "Test 2")) + .andExpect(status().isBadRequest()); + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", "bundle name") + .param("filterMetadata", "dc.title", "dc.date.issued", "dc.description") + .param("filterMetadataValue", "Test", "Test 2")) + .andExpect(status().isBadRequest()); + + } + + @Test + public void findByFakeItemId() throws Exception { + + String fakeId = "9cc8104e-5337-4305-b4ce-b578eb1b24ba"; + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", fakeId) + .param("name", "bundle name") + .param("filterMetadata", "dc.title") + .param("filterMetadataValue", "test")) + .andExpect(status().isUnprocessableEntity()); + + } + + @Test + public void findByItemIdWithNoMatchedBitstreams() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1").build(); + + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .build(); + + Bundle license = BundleBuilder.createBundle(context, publicItem1) + .withName("LICENSE") + .build(); + + String bitstreamContent1 = "This is an archived bitstream"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, license, is) + .withName("license bitstream name") + .withMimeType("text/plain") + .build(); + } + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", license.getName()) + .param("filterMetadata", "dc.title") + .param("filterMetadataValue", "wrong value")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + } + + @Test + public void findByItemIdAndBundleNameAndMetadataValue() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .build(); + + Bundle license = BundleBuilder.createBundle(context, publicItem1) + .withName("LICENSE") + .build(); + + String bitstreamContent1 = "This is an archived bitstream"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, license, is) + .withName("this is a test") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "This is an license bitstream"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, license, is) + .withName("this is a test 2") + .withMimeType("text/plain") + .build(); + } + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", license.getName()) + .param("filterMetadata", "dc.title") + .param("filterMetadataValue", "this is a test") + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(1))) + .andExpect(jsonPath("$._embedded.bitstreams", hasSize(1))) + .andExpect(jsonPath("$._embedded.bitstreams", contains( + BitstreamMatcher.matchBitstreamEntry(bitstream1) + ))); + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", license.getName()) + .param("filterMetadata", "dc.title") + .param("filterMetadataValue", "([a-z]+ [a-z]+ [a-z]+ [a-z]+)") + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(1))) + .andExpect(jsonPath("$._embedded.bitstreams", hasSize(1))) + .andExpect(jsonPath("$._embedded.bitstreams", contains( + BitstreamMatcher.matchBitstreamEntry(bitstream1) + ))); + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", license.getName()) + .param("filterMetadata", "dc.title") + .param("filterMetadataValue", "(this is a test.*)") + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(2))) + .andExpect(jsonPath("$._embedded.bitstreams", hasSize(2))) + .andExpect(jsonPath("$._embedded.bitstreams", containsInAnyOrder( + BitstreamMatcher.matchBitstreamEntry(bitstream1), + BitstreamMatcher.matchBitstreamEntry(bitstream2) + ))); + + } + + @Test + public void findByItemIdAndBundleName() throws Exception { + + context.turnOffAuthorisationSystem(); + + 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(); + + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .build(); + + Bundle license = BundleBuilder.createBundle(context, publicItem1) + .withName("LICENSE") + .build(); + + Bundle original = BundleBuilder.createBundle(context, publicItem1) + .withName("ORIGINAL") + .build(); + + String bitstreamContent1 = "This is an archived bitstream"; + Bitstream bitstream1 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent1, CharEncoding.UTF_8)) { + bitstream1 = BitstreamBuilder. + createBitstream(context, license, is) + .withName("this is a test") + .withMimeType("text/plain") + .build(); + } + + String bitstreamContent2 = "This is an original bitstream"; + Bitstream bitstream2 = null; + try (InputStream is = IOUtils.toInputStream(bitstreamContent2, CharEncoding.UTF_8)) { + bitstream2 = BitstreamBuilder. + createBitstream(context, original, is) + .withName("original bitstream name") + .withMimeType("text/plain") + .build(); + } + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", license.getName()) + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(1))) + .andExpect(jsonPath("$._embedded.bitstreams", hasSize(1))) + .andExpect(jsonPath("$._embedded.bitstreams", contains( + BitstreamMatcher.matchBitstreamEntry(bitstream1) + ))); + + } + + @Test + public void searchByItemWithManyFilterMetadata() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Test") + .withIssueDate("2010-10-17") + .withAuthor("Smith, Donald") + .build(); + + Bundle license = BundleBuilder.createBundle(context, publicItem1) + .withName("LICENSE") + .build(); + + Bitstream bitstream1 = BitstreamBuilder.createBitstream(context, license, InputStream.nullInputStream()) + .withName("this is a test") + .withType("Image") + .withMimeType("text/plain") + .build(); + + Bitstream bitstream2 = BitstreamBuilder.createBitstream(context, license, InputStream.nullInputStream()) + .withName("this is a test") + .withType("Personal Picture") + .withMimeType("text/plain") + .build(); + + Bitstream bitstream3 = BitstreamBuilder.createBitstream(context, license, InputStream.nullInputStream()) + .withName("this is a test 2") + .withType("Personal Picture") + .withMimeType("text/plain") + .build(); + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", license.getName()) + .param("filterMetadata", "dc.title") + .param("filterMetadataValue", "this is a test") + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(2))) + .andExpect(jsonPath("$._embedded.bitstreams", containsInAnyOrder( + BitstreamMatcher.matchBitstreamEntry(bitstream1), + BitstreamMatcher.matchBitstreamEntry(bitstream2)))); + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", license.getName()) + .param("filterMetadata", "dc.type") + .param("filterMetadataValue", "Personal Picture") + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(2))) + .andExpect(jsonPath("$._embedded.bitstreams", containsInAnyOrder( + BitstreamMatcher.matchBitstreamEntry(bitstream2), + BitstreamMatcher.matchBitstreamEntry(bitstream3)))); + + getClient().perform(get("/api/core/bitstreams/search/byItemId") + .param("uuid", publicItem1.getID().toString()) + .param("name", license.getName()) + .param("filterMetadata", "dc.title", "dc.type") + .param("filterMetadataValue", "this is a test", "Personal Picture") + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(1))) + .andExpect(jsonPath("$._embedded.bitstreams", hasSize(1))) + .andExpect(jsonPath("$._embedded.bitstreams", contains( + BitstreamMatcher.matchBitstreamEntry(bitstream2)))); + + } }