Merge pull request #3309 from atmire/w2p-80217_Link-to-item-thumbnail

Add endpoints for Item/Bitstream thumbnails
This commit is contained in:
Tim Donohue
2021-06-30 13:56:31 -05:00
committed by GitHub
10 changed files with 505 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ import java.sql.SQLException;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.apache.commons.collections4.CollectionUtils;
@@ -409,6 +410,25 @@ public class BitstreamServiceImpl extends DSpaceObjectServiceImpl<Bitstream> imp
return null;
}
@Override
public Bitstream getThumbnail(Context context, Bitstream bitstream) throws SQLException {
Pattern pattern = Pattern.compile("^" + bitstream.getName() + ".([^.]+)$");
for (Bundle bundle : bitstream.getBundles()) {
for (Item item : bundle.getItems()) {
for (Bundle thumbnails : itemService.getBundles(item, "THUMBNAIL")) {
for (Bitstream thumbnail : thumbnails.getBitstreams()) {
if (pattern.matcher(thumbnail.getName()).matches()) {
return thumbnail;
}
}
}
}
}
return null;
}
@Override
public BitstreamFormat getFormat(Context context, Bitstream bitstream) throws SQLException {
if (bitstream.getBitstreamFormat() == null) {

View File

@@ -209,6 +209,8 @@ public interface BitstreamService extends DSpaceObjectService<Bitstream>, DSpace
public Bitstream getFirstBitstream(Item item, String bundleName) throws SQLException;
public Bitstream getThumbnail(Context context, Bitstream bitstream) throws SQLException;
public BitstreamFormat getFormat(Context context, Bitstream bitstream) throws SQLException;
public Iterator<Bitstream> findByStoreNumber(Context context, Integer storeNumber) throws SQLException;

View File

@@ -23,6 +23,10 @@ import com.fasterxml.jackson.annotation.JsonProperty.Access;
@LinkRest(
name = BitstreamRest.FORMAT,
method = "getFormat"
),
@LinkRest(
name = BitstreamRest.THUMBNAIL,
method = "getThumbnail"
)
})
public class BitstreamRest extends DSpaceObjectRest {
@@ -32,6 +36,7 @@ public class BitstreamRest extends DSpaceObjectRest {
public static final String BUNDLE = "bundle";
public static final String FORMAT = "format";
public static final String THUMBNAIL = "thumbnail";
private String bundleName;

View File

@@ -40,6 +40,10 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@LinkRest(
name = ItemRest.TEMPLATE_ITEM_OF,
method = "getTemplateItemOf"
),
@LinkRest(
name = ItemRest.THUMBNAIL,
method = "getThumbnail"
)
})
public class ItemRest extends DSpaceObjectRest {
@@ -53,6 +57,7 @@ public class ItemRest extends DSpaceObjectRest {
public static final String RELATIONSHIPS = "relationships";
public static final String VERSION = "version";
public static final String TEMPLATE_ITEM_OF = "templateItemOf";
public static final String THUMBNAIL = "thumbnail";
private boolean inArchive = false;
private boolean discoverable = false;

View File

@@ -0,0 +1,55 @@
/**
* 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.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 the thumbnail Bitstream of a Bitstream
*/
@Component(BitstreamRest.CATEGORY + "." + BitstreamRest.NAME + "." + BitstreamRest.THUMBNAIL)
public class BitstreamThumbnailLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository {
@Autowired
BitstreamService bitstreamService;
@PreAuthorize("hasPermission(#bitstreamId, 'BITSTREAM', 'READ')")
public BitstreamRest getThumbnail(@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);
}
Bitstream thumbnail = bitstreamService.getThumbnail(context, bitstream);
if (thumbnail == null) {
return null;
}
return converter.toRest(thumbnail, projection);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.app.rest.model.BitstreamRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.content.Item;
import org.dspace.content.Thumbnail;
import org.dspace.content.service.ItemService;
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 the thumbnail Bitstream of an Item
*/
@Component(ItemRest.CATEGORY + "." + ItemRest.NAME + "." + ItemRest.THUMBNAIL)
public class ItemThumbnailLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository {
@Autowired
ItemService itemService;
@PreAuthorize("hasPermission(#itemId, 'ITEM', 'READ')")
public BitstreamRest getThumbnail(@Nullable HttpServletRequest request,
UUID itemId,
@Nullable Pageable optionalPageable,
Projection projection) {
try {
Context context = obtainContext();
Item item = itemService.find(context, itemId);
if (item == null) {
throw new ResourceNotFoundException("No such item: " + itemId);
}
Thumbnail thumbnail = itemService.getThumbnail(context, item, false);
if (thumbnail == null) {
return null;
}
return converter.toRest(thumbnail.getThumb(), projection);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -11,6 +11,7 @@ import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata;
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.is;
import static org.hamcrest.Matchers.not;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -46,6 +47,7 @@ import org.dspace.content.service.BitstreamFormatService;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Constants;
import org.dspace.eperson.EPerson;
import org.hamcrest.Matchers;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -1515,4 +1517,180 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest
.andExpect(status().isNoContent());
}
@Test
public void thumbnailEndpointTest() throws Exception {
// Given an Item
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1").build();
Item item = ItemBuilder.createItem(context, col1)
.withTitle("Test item -- thumbnail")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.build();
Bundle originalBundle = BundleBuilder.createBundle(context, item)
.withName(Constants.DEFAULT_BUNDLE_NAME)
.build();
Bundle thumbnailBundle = BundleBuilder.createBundle(context, item)
.withName("THUMBNAIL")
.build();
InputStream is = IOUtils.toInputStream("dummy", "utf-8");
// With an ORIGINAL Bitstream & matching THUMBNAIL Bitstream
Bitstream bitstream = BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test.pdf")
.withMimeType("application/pdf")
.build();
Bitstream thumbnail = BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test.pdf.jpg")
.withMimeType("image/jpeg")
.build();
context.restoreAuthSystemState();
String tokenAdmin = getAuthToken(admin.getEmail(), password);
getClient(tokenAdmin).perform(get("/api/core/bitstreams/" + bitstream.getID() + "/thumbnail"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.uuid", Matchers.is(thumbnail.getID().toString())))
.andExpect(jsonPath("$.type", is("bitstream")));
}
@Test
public void thumbnailEndpointMultipleThumbnailsWithPrimaryBitstreamTest() throws Exception {
// Given an Item
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1").build();
Item item = ItemBuilder.createItem(context, col1)
.withTitle("Test item -- thumbnail")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.build();
Bundle originalBundle = BundleBuilder.createBundle(context, item)
.withName(Constants.DEFAULT_BUNDLE_NAME)
.build();
Bundle thumbnailBundle = BundleBuilder.createBundle(context, item)
.withName("THUMBNAIL")
.build();
InputStream is = IOUtils.toInputStream("dummy", "utf-8");
// With multiple ORIGINAL Bitstreams & matching THUMBNAIL Bitstreams
Bitstream bitstream1 = BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test1.pdf")
.withMimeType("application/pdf")
.build();
Bitstream bitstream2 = BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test2.pdf")
.withMimeType("application/pdf")
.build();
Bitstream primaryBitstream = BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test3.pdf")
.withMimeType("application/pdf")
.build();
Bitstream thumbnail1 = BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test1.pdf.jpg")
.withMimeType("image/jpeg")
.build();
Bitstream thumbnail2 = BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test2.pdf.jpg")
.withMimeType("image/jpeg")
.build();
Bitstream primaryThumbnail = BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test3.pdf.jpg")
.withMimeType("image/jpeg")
.build();
// and a primary Bitstream
originalBundle.setPrimaryBitstreamID(primaryBitstream);
context.restoreAuthSystemState();
// Bitstream thumbnail endpoints should link to the right thumbnail Bitstreams
getClient().perform(get("/api/core/bitstreams/" + primaryBitstream.getID() + "/thumbnail"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.uuid", Matchers.is(primaryThumbnail.getID().toString())))
.andExpect(jsonPath("$.type", is("bitstream")));
getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID() + "/thumbnail"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.uuid", Matchers.is(thumbnail1.getID().toString())))
.andExpect(jsonPath("$.type", is("bitstream")));
getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID() + "/thumbnail"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.uuid", Matchers.is(thumbnail2.getID().toString())))
.andExpect(jsonPath("$.type", is("bitstream")));
}
@Test
public void thumbnailEndpointItemWithoutThumbnailsTest() throws Exception {
// Given an Item
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1").build();
Item item = ItemBuilder.createItem(context, col1)
.withTitle("Test item -- thumbnail")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.build();
Bundle originalBundle = BundleBuilder.createBundle(context, item)
.withName(Constants.DEFAULT_BUNDLE_NAME)
.build();
// With an empty THUMBNAIL bundle
Bundle thumbnailBundle = BundleBuilder.createBundle(context, item)
.withName("THUMBNAIL")
.build();
InputStream is = IOUtils.toInputStream("dummy", "utf-8");
Bitstream bitstream = BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test.pdf")
.withMimeType("application/pdf")
.build();
context.restoreAuthSystemState();
// Should fail with HTTP 204
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/thumbnail"))
.andExpect(status().isNoContent());
// With a THUMBNAIL bitstream that doesn't match the ORIGINAL Bitstream's name
context.turnOffAuthorisationSystem();
BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("random.pdf.jpg")
.withMimeType("image/jpeg")
.build();
context.restoreAuthSystemState();
// Should still fail with HTTP 204
getClient().perform(get("/api/core/bitstreams/" + bitstream.getID() + "/thumbnail"))
.andExpect(status().isNoContent());
}
}

View File

@@ -73,6 +73,7 @@ import org.dspace.content.Relationship;
import org.dspace.content.RelationshipType;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.service.CollectionService;
import org.dspace.core.Constants;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.hamcrest.Matcher;
@@ -3868,4 +3869,178 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
}
@Test
public void thumbnailEndpointTest() throws Exception {
// Given an Item
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1").build();
Item item = ItemBuilder.createItem(context, col1)
.withTitle("Test item -- thumbnail")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.build();
Bundle originalBundle = BundleBuilder.createBundle(context, item)
.withName(Constants.DEFAULT_BUNDLE_NAME)
.build();
Bundle thumbnailBundle = BundleBuilder.createBundle(context, item)
.withName("THUMBNAIL")
.build();
InputStream is = IOUtils.toInputStream("dummy", "utf-8");
// With two ORIGINAL Bitstreams with matching THUMBNAIL Bitstreams
BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test1.pdf")
.withMimeType("application/pdf")
.build();
BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test2.pdf")
.withMimeType("application/pdf")
.build();
Bitstream thumbnail = BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test1.pdf.jpg")
.withMimeType("image/jpeg")
.build();
BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test2.pdf.jpg")
.withMimeType("image/jpeg")
.build();
context.restoreAuthSystemState();
String tokenAdmin = getAuthToken(admin.getEmail(), password);
// Item's thumbnail endpoint should return the first ORIGINAL Bitstream's thumbnail
getClient(tokenAdmin).perform(get("/api/core/items/" + item.getID() + "/thumbnail"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.uuid", Matchers.is(thumbnail.getID().toString())))
.andExpect(jsonPath("$.type", is("bitstream")));
}
@Test
public void thumbnailEndpointMultipleThumbnailsWithPrimaryBitstreamTest() throws Exception {
// Given an Item
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1").build();
Item item = ItemBuilder.createItem(context, col1)
.withTitle("Test item -- thumbnail")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.build();
Bundle originalBundle = BundleBuilder.createBundle(context, item)
.withName(Constants.DEFAULT_BUNDLE_NAME)
.build();
Bundle thumbnailBundle = BundleBuilder.createBundle(context, item)
.withName("THUMBNAIL")
.build();
InputStream is = IOUtils.toInputStream("dummy", "utf-8");
// With two ORIGINAL Bitstreams with matching THUMBNAIL Bitstreams
BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test1.pdf")
.withMimeType("application/pdf")
.build();
BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test2.pdf")
.withMimeType("application/pdf")
.build();
Bitstream primaryBitstream = BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test3.pdf")
.withMimeType("application/pdf")
.build();
BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test1.pdf.jpg")
.withMimeType("image/jpeg")
.build();
BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test2.pdf.jpg")
.withMimeType("image/jpeg")
.build();
Bitstream primaryThumbnail = BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("test3.pdf.jpg")
.withMimeType("image/jpeg")
.build();
// and a primary Bitstream (not the first)
originalBundle.setPrimaryBitstreamID(primaryBitstream);
context.restoreAuthSystemState();
// Item's thumbnail endpoint should link to the primary Bitstream's thumbnail
getClient().perform(get("/api/core/items/" + item.getID() + "/thumbnail"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.uuid", Matchers.is(primaryThumbnail.getID().toString())))
.andExpect(jsonPath("$.type", is("bitstream")));
}
@Test
public void thumbnailEndpointItemWithoutThumbnailsTest() throws Exception {
// Given an Item
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection 1").build();
Item item = ItemBuilder.createItem(context, col1)
.withTitle("Test item -- thumbnail")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.build();
Bundle originalBundle = BundleBuilder.createBundle(context, item)
.withName(Constants.DEFAULT_BUNDLE_NAME)
.build();
// With an Empty THUMBNAIL Bundle
Bundle thumbnailBundle = BundleBuilder.createBundle(context, item)
.withName("THUMBNAIL")
.build();
InputStream is = IOUtils.toInputStream("dummy", "utf-8");
BitstreamBuilder.createBitstream(context, originalBundle, is)
.withName("test.pdf")
.withMimeType("application/pdf")
.build();
context.restoreAuthSystemState();
// Should fail with HTTP 204
getClient().perform(get("/api/core/items/" + item.getID() + "/thumbnail"))
.andExpect(status().isNoContent());
// With a THUMBNAIL bitstream that doesn't match the ORIGINAL Bitstream's name
context.turnOffAuthorisationSystem();
BitstreamBuilder.createBitstream(context, thumbnailBundle, is)
.withName("random.pdf.jpg")
.withMimeType("image/jpeg")
.build();
context.restoreAuthSystemState();
// Should still fail with HTTP 204
getClient().perform(get("/api/core/items/" + item.getID() + "/thumbnail"))
.andExpect(status().isNoContent());
}
}

View File

@@ -101,7 +101,8 @@ public class BitstreamMatcher {
public static Matcher<? super Object> matchFullEmbeds() {
return matchEmbeds(
"bundle",
"format"
"format",
"thumbnail"
);
}
@@ -113,7 +114,8 @@ public class BitstreamMatcher {
"bundle",
"content",
"format",
"self"
"self",
"thumbnail"
);
}

View File

@@ -55,7 +55,8 @@ public class ItemMatcher {
"owningCollection",
"version",
"relationships[]",
"templateItemOf"
"templateItemOf",
"thumbnail"
);
}
@@ -70,7 +71,8 @@ public class ItemMatcher {
"relationships",
"self",
"version",
"templateItemOf"
"templateItemOf",
"thumbnail"
);
}