diff --git a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusHelper.java b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusHelper.java index 2d782dc3b82..080033c6caa 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusHelper.java +++ b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusHelper.java @@ -10,6 +10,7 @@ import java.sql.SQLException; import java.util.Date; +import org.dspace.content.Bitstream; import org.dspace.content.Item; import org.dspace.core.Context; @@ -39,4 +40,27 @@ public String getAccessStatusFromItem(Context context, Item item, Date threshold * @throws SQLException An exception that provides information on a database access error or other errors. */ public String getEmbargoFromItem(Context context, Item item, Date threshold) throws SQLException; + + /** + * Calculate the access status for the bitstream. + * + * @param context the DSpace context + * @param bitstream the bitstream + * @param threshold the embargo threshold date + * @return an access status value + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public String getAccessStatusFromBitstream(Context context, Bitstream bitstream, Date threshold) + throws SQLException; + + /** + * Retrieve embargo information for the bitstream + * + * @param context the DSpace context + * @param bitstream the bitstream to check for embargo information + * @param threshold the embargo threshold date + * @return an embargo date + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public String getEmbargoFromBitstream(Context context, Bitstream bitstream, Date threshold) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java index e1f11285d84..977e7c2e4a4 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/access/status/AccessStatusServiceImpl.java @@ -11,6 +11,7 @@ import java.util.Date; import org.dspace.access.status.service.AccessStatusService; +import org.dspace.content.Bitstream; import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.core.service.PluginService; @@ -68,4 +69,14 @@ public String getAccessStatus(Context context, Item item) throws SQLException { public String getEmbargoFromItem(Context context, Item item) throws SQLException { return helper.getEmbargoFromItem(context, item, forever_date); } + + @Override + public String getAccessStatus(Context context, Bitstream bitstream) throws SQLException { + return helper.getAccessStatusFromBitstream(context, bitstream, forever_date); + } + + @Override + public String getEmbargoFromBitstream(Context context, Bitstream bitstream) throws SQLException { + return helper.getEmbargoFromBitstream(context, bitstream, forever_date); + } } diff --git a/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java b/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java index 5f0e6d8b259..81844f5017e 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java +++ b/dspace-api/src/main/java/org/dspace/access/status/DefaultAccessStatusHelper.java @@ -8,6 +8,7 @@ package org.dspace.access.status; import java.sql.SQLException; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Objects; @@ -47,6 +48,11 @@ public class DefaultAccessStatusHelper implements AccessStatusHelper { public static final String RESTRICTED = "restricted"; public static final String UNKNOWN = "unknown"; + // REST date fields use a plain ISO calendar date (e.g. "2050-01-01"), matching the format used + // elsewhere in the REST API (see ResourcePolicyRest). Date#toString() is locale/timezone-dependent + // and must not be used for values exposed over REST. + private static final String REST_DATE_FORMAT = "yyyy-MM-dd"; + protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); protected ResourcePolicyService resourcePolicyService = @@ -59,7 +65,7 @@ public DefaultAccessStatusHelper() { } /** - * Look at the item's policies to determine an access status value. + * Look at the item policies to determine an access status value. * It is also considering a date threshold for embargoes and restrictions. * * If the item is null, simply returns the "unknown" value. @@ -96,7 +102,7 @@ public String getAccessStatusFromItem(Context context, Item item, Date threshold } /** - * Look at the DSpace object's policies to determine an access status value. + * Look at the DSpace object policies to determine an access status value. * * If the object is null, returns the "metadata.only" value. * If any policy attached to the object is valid for the anonymous group, @@ -170,7 +176,8 @@ private String calculateAccessStatusForDso(Context context, DSpaceObject dso, Da * * @param context the DSpace context * @param item the item to embargo - * @return an access status value + * @param threshold the embargo threshold date + * @return an embargo date */ @Override public String getEmbargoFromItem(Context context, Item item, Date threshold) @@ -207,11 +214,73 @@ public String getEmbargoFromItem(Context context, Item item, Date threshold) embargoDate = this.retrieveShortestEmbargo(context, bitstream); - return embargoDate != null ? embargoDate.toString() : null; + return formatEmbargoDate(embargoDate); } /** + * Look at the policies attached directly to the bitstream to determine an access status value. + * It is also considering a date threshold for embargoes and restrictions. + * + * If the bitstream is null, simply returns the "unknown" value. * + * @param context the DSpace context + * @param bitstream the bitstream to check for embargoes + * @param threshold the embargo threshold date + * @return an access status value + */ + @Override + public String getAccessStatusFromBitstream(Context context, Bitstream bitstream, Date threshold) + throws SQLException { + if (bitstream == null) { + return UNKNOWN; + } + return calculateAccessStatusForDso(context, bitstream, threshold); + } + + /** + * Look at the policies of the bitstream to retrieve its embargo. + * + * If the bitstream is null, simply returns no embargo date. + * + * @param context the DSpace context + * @param bitstream the bitstream to embargo + * @param threshold the embargo threshold date + * @return an embargo date + */ + @Override + public String getEmbargoFromBitstream(Context context, Bitstream bitstream, Date threshold) + throws SQLException { + if (bitstream == null) { + return null; + } + // If Bitstream status is not "embargo" then return a null embargo date. + String accessStatus = getAccessStatusFromBitstream(context, bitstream, threshold); + if (!accessStatus.equals(EMBARGO)) { + return null; + } + Date embargoDate = this.retrieveShortestEmbargo(context, bitstream); + + return formatEmbargoDate(embargoDate); + } + + /** + * Format an embargo date for REST exposure as a plain ISO calendar date (yyyy-MM-dd), + * matching the format used elsewhere in the REST API. SimpleDateFormat is not thread-safe, + * so a new instance is created per call. + * + * @param date the date to format, may be null + * @return the formatted date, or null if the given date is null + */ + private String formatEmbargoDate(Date date) { + return date != null ? new SimpleDateFormat(REST_DATE_FORMAT).format(date) : null; + } + + /** + * Look at the read policies of a bitstream to retrieve the shortest active embargo date. + * + * @param context the DSpace context + * @param bitstream the bitstream + * @return the shortest embargo date, or null if there is none */ private Date retrieveShortestEmbargo(Context context, Bitstream bitstream) throws SQLException { Date embargoDate = null; diff --git a/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java b/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java index 2ed47bde4cd..e4bc88f0eb5 100644 --- a/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java +++ b/dspace-api/src/main/java/org/dspace/access/status/service/AccessStatusService.java @@ -9,6 +9,7 @@ import java.sql.SQLException; +import org.dspace.content.Bitstream; import org.dspace.content.Item; import org.dspace.core.Context; @@ -54,4 +55,24 @@ public interface AccessStatusService { * @throws SQLException An exception that provides information on a database access error or other errors. */ public String getEmbargoFromItem(Context context, Item item) throws SQLException; + + /** + * Calculate the access status for a Bitstream while considering the forever embargo date threshold. + * + * @param context the DSpace context + * @param bitstream the bitstream + * @return an access status value + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public String getAccessStatus(Context context, Bitstream bitstream) throws SQLException; + + /** + * Retrieve embargo information for the bitstream + * + * @param context the DSpace context + * @param bitstream the bitstream to check for embargo information + * @return an embargo date + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public String getEmbargoFromBitstream(Context context, Bitstream bitstream) throws SQLException; } diff --git a/dspace-api/src/test/java/org/dspace/access/status/AccessStatusServiceTest.java b/dspace-api/src/test/java/org/dspace/access/status/AccessStatusServiceTest.java index 87127f9cf8f..8aa46820352 100644 --- a/dspace-api/src/test/java/org/dspace/access/status/AccessStatusServiceTest.java +++ b/dspace-api/src/test/java/org/dspace/access/status/AccessStatusServiceTest.java @@ -8,8 +8,12 @@ package org.dspace.access.status; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import org.apache.logging.log4j.Logger; @@ -17,15 +21,20 @@ import org.dspace.access.status.factory.AccessStatusServiceFactory; import org.dspace.access.status.service.AccessStatusService; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; +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.InstallItemService; import org.dspace.content.service.ItemService; import org.dspace.content.service.WorkspaceItemService; +import org.dspace.core.Constants; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -40,6 +49,8 @@ public class AccessStatusServiceTest extends AbstractUnitTest { private Collection collection; private Community owningCommunity; private Item item; + private Bundle bundle; + private Bitstream bitstream; protected CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); @@ -47,6 +58,10 @@ public class AccessStatusServiceTest extends AbstractUnitTest { ContentServiceFactory.getInstance().getCollectionService(); protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + protected BundleService bundleService = + ContentServiceFactory.getInstance().getBundleService(); + protected BitstreamService bitstreamService = + ContentServiceFactory.getInstance().getBitstreamService(); protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); protected InstallItemService installItemService = @@ -71,6 +86,10 @@ public void init() { collection = collectionService.create(context, owningCommunity); item = installItemService.installItem(context, workspaceItemService.create(context, collection, true)); + bundle = bundleService.create(context, item, Constants.CONTENT_BUNDLE_NAME); + bitstream = bitstreamService.create(context, bundle, + new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8))); + bitstream.setName(context, "primary"); context.restoreAuthSystemState(); } catch (AuthorizeException ex) { log.error("Authorization Error in init", ex); @@ -78,6 +97,9 @@ public void init() { } catch (SQLException ex) { log.error("SQL Error in init", ex); fail("SQL Error in init: " + ex.getMessage()); + } catch (IOException ex) { + log.error("IO Error in init", ex); + fail("IO Error in init: " + ex.getMessage()); } } @@ -92,6 +114,16 @@ public void init() { @Override public void destroy() { context.turnOffAuthorisationSystem(); + try { + bitstreamService.delete(context, bitstream); + } catch (Exception e) { + // ignore + } + try { + bundleService.delete(context, bundle); + } catch (Exception e) { + // ignore + } try { itemService.delete(context, item); } catch (Exception e) { @@ -108,6 +140,8 @@ public void destroy() { // ignore } context.restoreAuthSystemState(); + bitstream = null; + bundle = null; item = null; collection = null; owningCommunity = null; @@ -123,4 +157,22 @@ public void testGetAccessStatus() throws Exception { String status = accessStatusService.getAccessStatus(context, item); assertNotEquals("testGetAccessStatus 0", status, DefaultAccessStatusHelper.UNKNOWN); } + + @Test + public void testGetEmbargoFromItem() throws Exception { + String embargo = accessStatusService.getEmbargoFromItem(context, item); + assertNull("testGetEmbargoFromItem 0", embargo); + } + + @Test + public void testGetAccessStatusFromBitstream() throws Exception { + String status = accessStatusService.getAccessStatus(context, bitstream); + assertNotEquals("testGetAccessStatusFromBitstream 0", status, DefaultAccessStatusHelper.UNKNOWN); + } + + @Test + public void testGetEmbargoFromBitstream() throws Exception { + String embargo = accessStatusService.getEmbargoFromBitstream(context, bitstream); + assertNull("testGetEmbargoFromBitstream 0", embargo); + } } diff --git a/dspace-api/src/test/java/org/dspace/access/status/DefaultAccessStatusHelperTest.java b/dspace-api/src/test/java/org/dspace/access/status/DefaultAccessStatusHelperTest.java index 1134990e84f..eadda2da48c 100644 --- a/dspace-api/src/test/java/org/dspace/access/status/DefaultAccessStatusHelperTest.java +++ b/dspace-api/src/test/java/org/dspace/access/status/DefaultAccessStatusHelperTest.java @@ -9,11 +9,13 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -208,6 +210,18 @@ public void testWithNullItem() throws Exception { assertThat("testWithNullItem 0", status, equalTo(DefaultAccessStatusHelper.UNKNOWN)); } + /** + * Test for a null bitstream + * @throws java.lang.Exception passed through. + */ + @Test + public void testWithNullBitstream() throws Exception { + String status = helper.getAccessStatusFromBitstream(context, null, threshold); + assertThat("testWithNullBitstream 0", status, equalTo(DefaultAccessStatusHelper.UNKNOWN)); + String embargoDate = helper.getEmbargoFromBitstream(context, null, threshold); + assertNull("testWithNullBitstream 1", embargoDate); + } + /** * Test for an item with no bundle * @throws java.lang.Exception passed through. @@ -246,6 +260,10 @@ public void testWithBitstream() throws Exception { context.restoreAuthSystemState(); String status = helper.getAccessStatusFromItem(context, itemWithBitstream, threshold); assertThat("testWithBitstream 0", status, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS)); + String bitstreamStatus = helper.getAccessStatusFromBitstream(context, bitstream, threshold); + assertThat("testWithBitstream 1", bitstreamStatus, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS)); + String bitstreamEmbargoDate = helper.getEmbargoFromBitstream(context, bitstream, threshold); + assertNull("testWithBitstream 2", bitstreamEmbargoDate); } /** @@ -274,7 +292,13 @@ public void testWithEmbargo() throws Exception { String status = helper.getAccessStatusFromItem(context, itemWithEmbargo, threshold); assertThat("testWithEmbargo 0", status, equalTo(DefaultAccessStatusHelper.EMBARGO)); String embargoDate = helper.getEmbargoFromItem(context, itemWithEmbargo, threshold); - assertThat("testWithEmbargo 1", embargoDate, equalTo(policy.getStartDate().toString())); + assertThat("testWithEmbargo 1", embargoDate, + equalTo(new SimpleDateFormat("yyyy-MM-dd").format(policy.getStartDate()))); + String bitstreamStatus = helper.getAccessStatusFromBitstream(context, bitstream, threshold); + assertThat("testWithEmbargo 2", bitstreamStatus, equalTo(DefaultAccessStatusHelper.EMBARGO)); + String bitstreamEmbargoDate = helper.getEmbargoFromBitstream(context, bitstream, threshold); + assertThat("testWithEmbargo 3", bitstreamEmbargoDate, + equalTo(new SimpleDateFormat("yyyy-MM-dd").format(policy.getStartDate()))); } /** @@ -302,6 +326,10 @@ public void testWithDateRestriction() throws Exception { context.restoreAuthSystemState(); String status = helper.getAccessStatusFromItem(context, itemWithDateRestriction, threshold); assertThat("testWithDateRestriction 0", status, equalTo(DefaultAccessStatusHelper.RESTRICTED)); + String bitstreamStatus = helper.getAccessStatusFromBitstream(context, bitstream, threshold); + assertThat("testWithDateRestriction 1", bitstreamStatus, equalTo(DefaultAccessStatusHelper.RESTRICTED)); + String bitstreamEmbargoDate = helper.getEmbargoFromBitstream(context, bitstream, threshold); + assertNull("testWithDateRestriction 2", bitstreamEmbargoDate); } /** @@ -374,7 +402,7 @@ public void testWithPrimaryAndMultipleBitstreams() throws Exception { context.turnOffAuthorisationSystem(); Bundle bundle = bundleService.create(context, itemWithPrimaryAndMultipleBitstreams, Constants.CONTENT_BUNDLE_NAME); - bitstreamService.create(context, bundle, + Bitstream otherBitstream = bitstreamService.create(context, bundle, new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8))); Bitstream primaryBitstream = bitstreamService.create(context, bundle, new ByteArrayInputStream("1".getBytes(StandardCharsets.UTF_8))); @@ -393,7 +421,14 @@ public void testWithPrimaryAndMultipleBitstreams() throws Exception { String status = helper.getAccessStatusFromItem(context, itemWithPrimaryAndMultipleBitstreams, threshold); assertThat("testWithPrimaryAndMultipleBitstreams 0", status, equalTo(DefaultAccessStatusHelper.EMBARGO)); String embargoDate = helper.getEmbargoFromItem(context, itemWithPrimaryAndMultipleBitstreams, threshold); - assertThat("testWithPrimaryAndMultipleBitstreams 1", embargoDate, equalTo(policy.getStartDate().toString())); + assertThat("testWithPrimaryAndMultipleBitstreams 1", embargoDate, + equalTo(new SimpleDateFormat("yyyy-MM-dd").format(policy.getStartDate()))); + String primaryBitstreamStatus = helper.getAccessStatusFromBitstream(context, primaryBitstream, threshold); + assertThat("testWithPrimaryAndMultipleBitstreams 2", primaryBitstreamStatus, + equalTo(DefaultAccessStatusHelper.EMBARGO)); + String otherBitstreamStatus = helper.getAccessStatusFromBitstream(context, otherBitstream, threshold); + assertThat("testWithPrimaryAndMultipleBitstreams 3", otherBitstreamStatus, + equalTo(DefaultAccessStatusHelper.OPEN_ACCESS)); } /** @@ -425,5 +460,8 @@ public void testWithNoPrimaryAndMultipleBitstreams() throws Exception { assertThat("testWithNoPrimaryAndMultipleBitstreams 0", status, equalTo(DefaultAccessStatusHelper.OPEN_ACCESS)); String embargoDate = helper.getEmbargoFromItem(context, itemWithEmbargo, threshold); assertThat("testWithNoPrimaryAndMultipleBitstreams 1", embargoDate, equalTo(null)); + String otherBitstreamStatus = helper.getAccessStatusFromBitstream(context, anotherBitstream, threshold); + assertThat("testWithNoPrimaryAndMultipleBitstreams 2", otherBitstreamStatus, + equalTo(DefaultAccessStatusHelper.EMBARGO)); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataBitstreamWrapperConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataBitstreamWrapperConverter.java index e42c023f72a..09551b84389 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataBitstreamWrapperConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataBitstreamWrapperConverter.java @@ -7,9 +7,15 @@ */ package org.dspace.app.rest.converter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.access.status.DefaultAccessStatusHelper; +import org.dspace.access.status.service.AccessStatusService; import org.dspace.app.rest.model.MetadataBitstreamWrapperRest; import org.dspace.app.rest.model.wrapper.MetadataBitstreamWrapper; import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.core.Context; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -24,6 +30,8 @@ public class MetadataBitstreamWrapperConverter implements DSpaceConverter { + private static final Logger log = LogManager.getLogger(MetadataBitstreamWrapperConverter.class); + @Lazy @Autowired private ConverterService converter; @@ -32,6 +40,9 @@ public class MetadataBitstreamWrapperConverter implements DSpaceConverter getModelClass() { return MetadataBitstreamWrapper.class; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AccessStatusRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AccessStatusRest.java index c7dc2d11985..a2b82b8fc5a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AccessStatusRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AccessStatusRest.java @@ -18,6 +18,7 @@ public class AccessStatusRest implements RestModel { public static final String NAME = "accessStatus"; String status; + String embargoDate; @Override @JsonProperty(access = Access.READ_ONLY) @@ -33,10 +34,12 @@ public String getTypePlural() { public AccessStatusRest() { setStatus(null); + setEmbargoDate(null); } public AccessStatusRest(String status) { setStatus(status); + setEmbargoDate(null); } public String getStatus() { @@ -46,4 +49,12 @@ public String getStatus() { public void setStatus(String status) { this.status = status; } + + public String getEmbargoDate() { + return embargoDate; + } + + public void setEmbargoDate(String embargoDate) { + this.embargoDate = embargoDate; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BitstreamRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BitstreamRest.java index a1c3156a01a..3f64acfb1c1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BitstreamRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/BitstreamRest.java @@ -31,6 +31,10 @@ @LinkRest( name = BitstreamRest.CHECKSUM, method = "getChecksum" + ), + @LinkRest( + name = BitstreamRest.ACCESS_STATUS, + method = "getAccessStatus" ) }) public class BitstreamRest extends DSpaceObjectRest { @@ -42,6 +46,7 @@ public class BitstreamRest extends DSpaceObjectRest { public static final String FORMAT = "format"; public static final String THUMBNAIL = "thumbnail"; public static final String CHECKSUM = "checksum"; + public static final String ACCESS_STATUS = "accessStatus"; private String bundleName; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataBitstreamWrapperRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataBitstreamWrapperRest.java index 8e4d60d4bfa..9bfb77590f7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataBitstreamWrapperRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataBitstreamWrapperRest.java @@ -29,6 +29,8 @@ public class MetadataBitstreamWrapperRest extends BaseObjectRest { private String format; private String href; private boolean canPreview; + private String status; + private String embargoDate; public MetadataBitstreamWrapperRest(String name, String description, long fileSize, String checksum, List fileInfo, String format, String href, boolean canPreview) { @@ -109,6 +111,22 @@ public void setChecksum(String checksum) { this.checksum = checksum; } + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getEmbargoDate() { + return embargoDate; + } + + public void setEmbargoDate(String embargoDate) { + this.embargoDate = embargoDate; + } + @Override public String getCategory() { return CATEGORY; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamAccessStatusLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamAccessStatusLinkRepository.java new file mode 100644 index 00000000000..5c85cf4c7d6 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/BitstreamAccessStatusLinkRepository.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.app.rest.repository; + +import java.sql.SQLException; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.access.status.DefaultAccessStatusHelper; +import org.dspace.access.status.service.AccessStatusService; +import org.dspace.app.rest.model.AccessStatusRest; +import org.dspace.app.rest.model.BitstreamRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.content.Bitstream; +import org.dspace.content.service.BitstreamService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Link repository for calculating the access status of a Bitstream, + * including the embargo date. + */ +@Component(BitstreamRest.CATEGORY + "." + BitstreamRest.NAME + "." + BitstreamRest.ACCESS_STATUS) +public class BitstreamAccessStatusLinkRepository extends AbstractDSpaceRestRepository + implements LinkRestRepository { + + @Autowired + BitstreamService bitstreamService; + + @Autowired + AccessStatusService accessStatusService; + + @PreAuthorize("hasPermission(#bitstreamId, 'BITSTREAM', 'METADATA_READ')") + public AccessStatusRest getAccessStatus(@Nullable HttpServletRequest request, + UUID bitstreamId, + @Nullable Pageable optionalPageable, + Projection projection) { + try { + Context context = obtainContext(); + Bitstream bitstream = bitstreamService.find(context, bitstreamId); + if (bitstream == null) { + throw new ResourceNotFoundException("No such bitstream: " + bitstreamId); + } + AccessStatusRest accessStatusRest = new AccessStatusRest(); + String status = accessStatusService.getAccessStatus(context, bitstream); + if (DefaultAccessStatusHelper.EMBARGO.equals(status)) { + String embargoDate = accessStatusService.getEmbargoFromBitstream(context, bitstream); + accessStatusRest.setEmbargoDate(embargoDate); + } + accessStatusRest.setStatus(status); + return accessStatusRest; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemAccessStatusLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemAccessStatusLinkRepository.java index b2660f51e09..372fc16579d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemAccessStatusLinkRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemAccessStatusLinkRepository.java @@ -13,6 +13,7 @@ import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; +import org.dspace.access.status.DefaultAccessStatusHelper; import org.dspace.access.status.service.AccessStatusService; import org.dspace.app.rest.model.AccessStatusRest; import org.dspace.app.rest.model.ItemRest; @@ -52,6 +53,10 @@ public AccessStatusRest getAccessStatus(@Nullable HttpServletRequest request, } AccessStatusRest accessStatusRest = new AccessStatusRest(); String accessStatus = accessStatusService.getAccessStatus(context, item); + if (DefaultAccessStatusHelper.EMBARGO.equals(accessStatus)) { + String embargoDate = accessStatusService.getEmbargoFromItem(context, item); + accessStatusRest.setEmbargoDate(embargoDate); + } accessStatusRest.setStatus(accessStatus); return accessStatusRest; } catch (SQLException e) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataBitstreamRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataBitstreamRestRepository.java index 4778cec279e..e6c85514a9d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataBitstreamRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/MetadataBitstreamRestRepository.java @@ -419,6 +419,11 @@ private boolean findOutCanPreview(Context context, Bitstream bitstream) throws S return true; } catch (MissingLicenseAgreementException e) { return false; + } catch (AuthorizeException e) { + // The requesting user (e.g. anonymous) doesn't have READ on this bitstream at all - + // e.g. it's under embargo. Same "can't preview" outcome as the license case above; + // this used to propagate uncaught and 500 the entire file listing for the item. + return false; } } 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 60379713d4f..187b56fc1d5 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 @@ -33,6 +33,7 @@ import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; +import org.dspace.access.status.DefaultAccessStatusHelper; import org.dspace.app.rest.matcher.BitstreamFormatMatcher; import org.dspace.app.rest.matcher.BitstreamMatcher; import org.dspace.app.rest.matcher.BundleMatcher; @@ -2929,6 +2930,53 @@ public void deleteBitstreamsInBulk_communityAdmin() throws Exception { .andExpect(status().isNoContent()); } + @Test + public void findAccessStatusForBitstreamBadRequestTest() throws Exception { + getClient().perform(get("/api/core/bitstreams/{uuid}/accessStatus", "1")) + .andExpect(status().isBadRequest()); + } + + @Test + public void findAccessStatusForBitstreamNotFoundTest() throws Exception { + // Unlike the item-level accessStatus link (gated on plain 'READ'), the bitstream-level link is + // gated on 'METADATA_READ' (see BitstreamMetadataReadPermissionEvaluatorPlugin), whose evaluator + // denies permission outright when the target bitstream cannot be resolved, so an anonymous request + // never reaches the controller's not-found check and instead gets a 401. + UUID fakeUUID = UUID.randomUUID(); + getClient().perform(get("/api/core/bitstreams/{uuid}/accessStatus", fakeUUID)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findAccessStatusForBitstreamTest() 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 item 1") + .build(); + String bitstreamContent = "ThisIsSomeDummyText"; + 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(); + + // Bitstream access status should still be accessible by anonymous request + getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/accessStatus")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.type", is("accessStatus"))) + .andExpect(jsonPath("$.status", is(DefaultAccessStatusHelper.OPEN_ACCESS))); + } + public boolean bitstreamExists(String token, Bitstream ...bitstreams) throws Exception { for (Bitstream bitstream : bitstreams) { if (getClient(token).perform(get("/api/core/bitstreams/" + bitstream.getID())) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/MetadataBitstreamRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/MetadataBitstreamRestRepositoryIT.java index c966ecfa58e..a14e75a1c0d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/MetadataBitstreamRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/MetadataBitstreamRestRepositoryIT.java @@ -10,6 +10,7 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -22,6 +23,7 @@ import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.dspace.access.status.DefaultAccessStatusHelper; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.util.Util; import org.dspace.authorize.service.AuthorizeService; @@ -124,9 +126,37 @@ public void findByHandle() throws Exception { .andExpect(jsonPath("$._embedded.metadatabitstreams[*].checksum") .value(Matchers.containsInAnyOrder(Matchers.containsString(bts.getChecksum())))) .andExpect(jsonPath("$._embedded.metadatabitstreams[*].href") - .value(Matchers.containsInAnyOrder(Matchers.containsString(url)))); + .value(Matchers.containsInAnyOrder(Matchers.containsString(url)))) + .andExpect(jsonPath("$._embedded.metadatabitstreams[*].status") + .value(Matchers.containsInAnyOrder(DefaultAccessStatusHelper.OPEN_ACCESS))) + .andExpect(jsonPath("$._embedded.metadatabitstreams[0].embargoDate").value(nullValue())); + + + } + @Test + public void findByHandleEmbargoedBitstream() throws Exception { + context.turnOffAuthorisationSystem(); + Bitstream embargoedBitstream; + String bitstreamContent = "ThisIsSomeEmbargoedText"; + try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) { + embargoedBitstream = BitstreamBuilder.createBitstream(context, publicItem, is) + .withName("Embargoed Bitstream") + .withDescription("Embargoed description") + .withMimeType("application/x-gzip") + .withEmbargoPeriod("3 months") + .build(); + } + context.restoreAuthSystemState(); + getClient().perform(get(METADATABITSTREAM_SEARCH_BY_HANDLE_ENDPOINT) + .param("handle", publicItem.getHandle()) + .param("fileGrpType", FILE_GRP_TYPE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.metadatabitstreams[?(@.name == '" + embargoedBitstream.getName() + + "')].status", Matchers.contains(DefaultAccessStatusHelper.EMBARGO))) + .andExpect(jsonPath("$._embedded.metadatabitstreams[?(@.name == '" + embargoedBitstream.getName() + + "')].embargoDate", Matchers.contains(notNullValue()))); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java index 9c9c0513c18..97746e75140 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java @@ -103,7 +103,8 @@ public static Matcher matchFullEmbeds() { "bundle", "format", "thumbnail", - "checksum" + "checksum", + "accessStatus" ); } @@ -117,7 +118,8 @@ public static Matcher matchLinks(UUID uuid) { "format", "self", "thumbnail", - "checksum" + "checksum", + "accessStatus" ); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/model/AccessStatusRestTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/model/AccessStatusRestTest.java index 7dfe3e69e0e..471e5d3704b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/model/AccessStatusRestTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/model/AccessStatusRestTest.java @@ -15,7 +15,7 @@ import org.junit.Test; /** - * Test the AccessStatusRestTest class + * Test the AccessStatusRest class */ public class AccessStatusRestTest { @@ -36,4 +36,15 @@ public void testAccessStatusIsNotNullAfterStatusSet() throws Exception { accessStatusRest.setStatus(DefaultAccessStatusHelper.UNKNOWN); assertNotNull(accessStatusRest.getStatus()); } + + @Test + public void testEmbargoDateIsNullBeforeEmbargoDateSet() throws Exception { + assertNull(accessStatusRest.getEmbargoDate()); + } + + @Test + public void testEmbargoDateIsNotNullAfterEmbargoDateSet() throws Exception { + accessStatusRest.setEmbargoDate("2050-01-01"); + assertNotNull(accessStatusRest.getEmbargoDate()); + } }