From 1b74558895e50c3c6264b201e37e9d51d4b0433f Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Thu, 2 Nov 2023 16:57:13 -0400 Subject: [PATCH 1/7] Add service for creating iiif v3 manifests --- web-services-app/pom.xml | 5 + .../processing/IiifV3ManifestService.java | 208 ++++++++++++++++++ .../rest/IiifV3ManifestController.java | 71 ++++++ .../processing/IiifV3ManifestServiceTest.java | 190 ++++++++++++++++ .../rest/IiifV3ManifestControllerTest.java | 117 ++++++++++ 5 files changed, 591 insertions(+) create mode 100644 web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java create mode 100644 web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java create mode 100644 web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java create mode 100644 web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java diff --git a/web-services-app/pom.xml b/web-services-app/pom.xml index 35e44b0794..d17371dda8 100644 --- a/web-services-app/pom.xml +++ b/web-services-app/pom.xml @@ -166,6 +166,11 @@ wiremock-jre8 test + + info.freelibrary + jiiify-presentation-v3 + 0.12.3 + jakarta.servlet diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java new file mode 100644 index 0000000000..63639443a6 --- /dev/null +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java @@ -0,0 +1,208 @@ +package edu.unc.lib.boxc.web.services.processing; + +import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; +import edu.unc.lib.boxc.common.util.URIUtil; +import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.search.api.models.ContentObjectRecord; +import edu.unc.lib.boxc.search.api.models.Datastream; +import edu.unc.lib.boxc.web.common.services.AccessCopiesService; +import info.freelibrary.iiif.presentation.v3.AnnotationPage; +import info.freelibrary.iiif.presentation.v3.Canvas; +import info.freelibrary.iiif.presentation.v3.ImageContent; +import info.freelibrary.iiif.presentation.v3.Manifest; +import info.freelibrary.iiif.presentation.v3.PaintingAnnotation; +import info.freelibrary.iiif.presentation.v3.properties.Label; +import info.freelibrary.iiif.presentation.v3.properties.Metadata; +import info.freelibrary.iiif.presentation.v3.properties.RequiredStatement; +import info.freelibrary.iiif.presentation.v3.services.ImageService3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Service for generating iiif v3 manifests for repository object + * @author bbpennel + */ +public class IiifV3ManifestService { + private static final Logger log = LoggerFactory.getLogger(IiifV3ManifestService.class); + private AccessCopiesService accessCopiesService; + private String baseIiifv3Path; + private String baseAccessPath; + private String baseServicesPath; + + /** + * Constructs a manifest record for the object identified by the provided PID + * @param pid + * @param agent + * @return + */ + public Manifest buildManifest(PID pid, AgentPrincipals agent) { + var contentObjs = accessCopiesService.listViewableFiles(pid, agent.getPrincipals()); + if (contentObjs.size() == 0) { + throw new NotFoundException("No objects were found for inclusion in manifest for object " + pid.getId()); + } + log.debug("Constructing manifest for {} containing {} items", pid.getId(), contentObjs.size()); + ContentObjectRecord rootObj = contentObjs.get(0); + var manifest = new Manifest(getManifestPath(rootObj), new Label(getTitle(rootObj))); + manifest.setMetadata(buildMetadata(rootObj)); + addAttribution(manifest, rootObj); + + addItems(manifest, contentObjs); + + return manifest; + } + + private List buildMetadata(ContentObjectRecord rootObj) { + var metadataList = new ArrayList(); + String abstractText = rootObj.getAbstractText(); + if (abstractText != null) { + metadataList.add(new Metadata("description", abstractText)); + } + addMetadataField(metadataList, "Creators", rootObj.getCreator()); + addMetadataField(metadataList, "Subjects", rootObj.getSubject()); + addMetadataField(metadataList, "Languages", rootObj.getLanguage()); + metadataList.add(new Metadata("", "View full record")); + return metadataList; + } + + private void addMetadataField(List metadataList, String fieldName, List values) { + if (!CollectionUtils.isEmpty(values)) { + metadataList.add(new Metadata(fieldName, String.join(", ", values))); + } + } + + private String getTitle(ContentObjectRecord contentObj) { + String title = contentObj.getTitle(); + return (title != null) ? title : ""; + } + + private void addAttribution(Manifest manifest, ContentObjectRecord rootObj) { + String attribution = "University of North Carolina Libraries, Digital Collections Repository"; + String collection = rootObj.getParentCollectionName(); + if (collection != null) { + attribution += " - Part of " + collection; + } + manifest.setRequiredStatement(new RequiredStatement("Attribution", attribution)); + } + + /** + * Add canvas items for each record in the set being processed + * @param manifest + * @param contentObjs + */ + private void addItems(Manifest manifest, List contentObjs) { + var canvases = new ArrayList(); + for (ContentObjectRecord contentObj : contentObjs) { + // Add canvases for any records with displayable content + if (hasViewableContent(contentObj)) { + canvases.add(createCanvas(contentObj)); + } + } + manifest.setCanvases(canvases); + } + + /** + * Constructs a canvas record for the provided object + * @param contentObj + * @return + */ + public Canvas createCanvas(ContentObjectRecord contentObj) { + String title = getTitle(contentObj); + String uuid = contentObj.getId(); + + var canvas = new Canvas(getCanvasPath(contentObj), title); + + // Set up thumbnail for the current item + var thumbnail = new ImageContent(makeThumbnailUrl(uuid)); + canvas.setThumbnails(thumbnail); + + // Children of canvas are AnnotationPages + var annoPage = new AnnotationPage(getAnnotationPagePath(contentObj)); + canvas.setPaintingPages(annoPage); + + // Child of the AnnotationPage is an Annotation, specifically a PaintingAnnotation in this case + var paintingAnno = new PaintingAnnotation(getAnnotationPath(contentObj), canvas); + annoPage.addAnnotations(paintingAnno); + + // Child of the Annotation is the content resource, which is an ImageContent + var imageContent = new ImageContent(getImagePath(contentObj)); + imageContent.setFormat("image/jpeg"); + paintingAnno.getBodies().add(imageContent); + + // Child of the content resource is an ImageService + var imageService = new ImageService3(ImageService3.Profile.LEVEL_TWO, getServicePath(contentObj)); + imageContent.setServices(imageService); + + // Set the dimensions of this item on appropriate elements + assignDimensions(contentObj, canvas, imageContent); + + return canvas; + } + + private void assignDimensions(ContentObjectRecord contentObj, Canvas canvas, ImageContent imageContent) { + Datastream fileDs = contentObj.getDatastreamObject(DatastreamType.ORIGINAL_FILE.getId()); + String extent = fileDs.getExtent(); + if (extent != null && !extent.equals("")) { + String[] imgDimensions = extent.split("x"); + var width = Integer.parseInt(imgDimensions[1]); // in the datastream, the width is second + var height = Integer.parseInt(imgDimensions[0]); + canvas.setWidthHeight(width, height); // Dimensions for the canvas + imageContent.setWidthHeight(width, height); // Dimensions for the actual image + } + } + + private String getManifestPath(ContentObjectRecord contentObj) { + return URIUtil.join(baseIiifv3Path, contentObj.getId(), "manifest"); + } + + private String getCanvasPath(ContentObjectRecord contentObj) { + return URIUtil.join(baseIiifv3Path, contentObj.getId(), "canvas"); + } + + private String getAnnotationPagePath(ContentObjectRecord contentObj) { + return URIUtil.join(baseIiifv3Path, contentObj.getId(), "page", "1"); + } + + private String getAnnotationPath(ContentObjectRecord contentObj) { + return URIUtil.join(baseIiifv3Path, contentObj.getId(), "annotation", "1"); + } + + private String getImagePath(ContentObjectRecord contentObj) { + return URIUtil.join(baseIiifv3Path, contentObj.getId(), "full", "max", "0", "default.jpg"); + } + + private String getServicePath(ContentObjectRecord contentObj) { + return URIUtil.join(baseIiifv3Path, contentObj.getId()); + } + + private String makeThumbnailUrl(String id) { + return URIUtil.join(baseServicesPath, "api", "thumb", id, "large"); + } + + private boolean hasViewableContent(ContentObjectRecord contentObj) { + var datastream = contentObj.getDatastreamObject(DatastreamType.JP2_ACCESS_COPY.getId()); + return datastream != null; + } + + public void setAccessCopiesService(AccessCopiesService accessCopiesService) { + this.accessCopiesService = accessCopiesService; + } + + public void setBaseIiifv3Path(String baseIiifv3Path) { + this.baseIiifv3Path = baseIiifv3Path; + } + + public void setBaseAccessPath(String baseAccessPath) { + this.baseAccessPath = baseAccessPath; + } + + public void setBaseServicesPath(String baseServicesPath) { + this.baseServicesPath = baseServicesPath; + } +} diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java new file mode 100644 index 0000000000..e33dd9f15c --- /dev/null +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java @@ -0,0 +1,71 @@ +package edu.unc.lib.boxc.web.services.rest; + +import edu.unc.lib.boxc.auth.api.Permission; +import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.api.services.DatastreamPermissionUtil; +import edu.unc.lib.boxc.auth.fcrepo.models.AgentPrincipalsImpl; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; +import edu.unc.lib.boxc.web.services.processing.IiifV3ManifestService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * Controller which handles iiif v3 requests + * + * @author bbpennel + */ +@Controller +@RequestMapping("/iiif/v3/") +public class IiifV3ManifestController { + private static final Logger log = LoggerFactory.getLogger(IiifV3ManifestController.class); + + @Autowired + private AccessControlService accessControlService; + + @Autowired + private IiifV3ManifestService manifestService; + + /** + * Handles requests for IIIF v3 manifests + * @param id + * @return Response containing the manifest + */ + @CrossOrigin + @GetMapping(value = "/{id}/manifest", produces = APPLICATION_JSON_VALUE) + @ResponseBody + public ResponseEntity getManifest(@PathVariable("id") String id) { + PID pid = PIDs.get(id); + assertHasAccess(pid); + + var manifest = manifestService.buildManifest(pid, AgentPrincipalsImpl.createFromThread()); + + return new ResponseEntity<>(manifest, HttpStatus.OK); + } + + private void assertHasAccess(PID pid) { + Permission permission = DatastreamPermissionUtil.getPermissionForDatastream(JP2_ACCESS_COPY); + + AgentPrincipals agent = AgentPrincipalsImpl.createFromThread(); + log.debug("Checking if user {} has access to {} belonging to object {}.", + agent.getUsername(), JP2_ACCESS_COPY, pid); + accessControlService.assertHasAccess(pid, agent.getPrincipals(), permission); + } + + public void setManifestService(IiifV3ManifestService manifestService) { + this.manifestService = manifestService; + } +} diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java new file mode 100644 index 0000000000..6274705181 --- /dev/null +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java @@ -0,0 +1,190 @@ +package edu.unc.lib.boxc.web.services.processing; + +import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; +import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; +import edu.unc.lib.boxc.model.api.ResourceType; +import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; +import edu.unc.lib.boxc.search.api.models.ContentObjectRecord; +import edu.unc.lib.boxc.search.solr.models.ContentObjectSolrRecord; +import edu.unc.lib.boxc.search.solr.models.DatastreamImpl; +import edu.unc.lib.boxc.web.common.services.AccessCopiesService; +import info.freelibrary.iiif.presentation.v3.Canvas; +import info.freelibrary.iiif.presentation.v3.ImageContent; +import info.freelibrary.iiif.presentation.v3.services.ImageService3; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; + +/** + * @author bbpennel + */ +public class IiifV3ManifestServiceTest { + private static final String IIIF_BASE = "http://example.com/iiif/v3/"; + private static final String SERVICES_BASE = "http://example.com/services/"; + private static final String ACCESS_BASE = "http://example.com/"; + private static final String WORK_ID = "5d72b84a-983c-4a45-8caa-dc9857987da2"; + private static final String FILE1_ID = "faffb3e1-85fc-451f-9075-c60fc7584c7b"; + private static final String FILE2_ID = "b6c51e59-d931-41d6-ba26-ec54ba9b2ef5"; + private static final String COLL_ID = "fdce64cb-6a6f-43bb-8ed2-58f3b60148bf"; + private static final PID WORK_PID = PIDs.get(WORK_ID); + + @Mock + private AccessCopiesService accessCopiesService; + @Mock + private AgentPrincipals agent; + @Mock + private AccessGroupSet principals; + private AutoCloseable closeable; + + private ContentObjectSolrRecord workObj; + + private IiifV3ManifestService manifestService; + + @BeforeEach + public void setup() { + closeable = openMocks(this); + manifestService = new IiifV3ManifestService(); + manifestService.setAccessCopiesService(accessCopiesService); + manifestService.setBaseIiifv3Path(IIIF_BASE); + manifestService.setBaseServicesPath(SERVICES_BASE); + manifestService.setBaseAccessPath(ACCESS_BASE); + + when(agent.getPrincipals()).thenReturn(principals); + + workObj = new ContentObjectSolrRecord(); + workObj.setId(WORK_ID); + workObj.setResourceType(ResourceType.Work.name()); + workObj.setTitle("Test Work"); + } + + private ContentObjectRecord createFileRecord(String id) { + var fileObj = new ContentObjectSolrRecord(); + fileObj.setId(id); + fileObj.setResourceType(ResourceType.File.name()); + fileObj.setTitle("File Object " + id); + var originalDs = new DatastreamImpl("original_file|image/jpeg|image.jpg|jpg|0|||240x750"); + var jp2Ds = new DatastreamImpl("jp2|image/jp2|image.jp2|jp2|0|||"); + fileObj.setDatastream(Arrays.asList(originalDs.toString(), jp2Ds.toString())); + return fileObj; + } + + @Test + public void buildManifestNoViewableTest() throws Exception { + when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList()); + assertThrows(NotFoundException.class, () -> { + manifestService.buildManifest(WORK_PID, agent); + }); + } + + @Test + public void buildManifestWorkWithoutViewableTest() throws Exception { + when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList(workObj)); + + var manifest = manifestService.buildManifest(WORK_PID, agent); + assertEquals("Test Work", manifest.getLabel().getString()); + assertEquals("http://example.com/iiif/v3/5d72b84a-983c-4a45-8caa-dc9857987da2/manifest", manifest.getID().toString()); + assertEquals("View full record", + manifest.getMetadata().get(0).getValue().getString()); + assertTrue(manifest.getCanvases().isEmpty()); + } + + @Test + public void buildManifestWorkWithViewableFilesTest() throws Exception { + var fileObj1 = createFileRecord(FILE1_ID); + var fileObj2 = createFileRecord(FILE2_ID); + when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList(workObj, fileObj1, fileObj2)); + + var manifest = manifestService.buildManifest(WORK_PID, agent); + assertEquals("Test Work", manifest.getLabel().getString()); + assertEquals("http://example.com/iiif/v3/5d72b84a-983c-4a45-8caa-dc9857987da2/manifest", + manifest.getID().toString()); + var canvases = manifest.getCanvases(); + assertEquals(2, canvases.size()); + assertFileCanvasPopulated(canvases.get(0), FILE1_ID); + assertFileCanvasPopulated(canvases.get(1), FILE2_ID); + } + + @Test + public void buildManifestViewableFileTest() throws Exception { + var fileObj1 = createFileRecord(FILE1_ID); + var filePid = PIDs.get(FILE1_ID); + when(accessCopiesService.listViewableFiles(filePid, principals)).thenReturn(Arrays.asList(fileObj1)); + + var manifest = manifestService.buildManifest(filePid, agent); + assertEquals("File Object faffb3e1-85fc-451f-9075-c60fc7584c7b", manifest.getLabel().getString()); + assertEquals("http://example.com/iiif/v3/faffb3e1-85fc-451f-9075-c60fc7584c7b/manifest", + manifest.getID().toString()); + var canvases = manifest.getCanvases(); + assertEquals(1, canvases.size()); + assertFileCanvasPopulated(canvases.get(0), FILE1_ID); + + assertEquals("View full record", + manifest.getMetadata().get(0).getValue().getString()); + } + + private void assertFileCanvasPopulated(Canvas fileCanvas, String expectedId) { + assertEquals("http://example.com/iiif/v3/" + expectedId + "/canvas", + fileCanvas.getID().toString()); + assertEquals(240, fileCanvas.getHeight()); + assertEquals(750, fileCanvas.getWidth()); + assertEquals("http://example.com/services/api/thumb/" + expectedId + "/large", + fileCanvas.getThumbnails().get(0).getID().toString()); + var annoPage = fileCanvas.getPaintingPages().get(0); + var annotation = annoPage.getAnnotations().get(0); + assertEquals("painting", annotation.getMotivation()); + + var imageContent = (ImageContent) annotation.getBodies().get(0); + assertEquals(240, imageContent.getHeight()); + assertEquals(750, imageContent.getWidth()); + assertEquals("image/jpeg", imageContent.getFormat().get().toString()); + var imageService = (ImageService3) imageContent.getServices().get(0); + assertEquals("http://example.com/iiif/v3/" + expectedId, imageService.getID().toString()); + assertEquals("level2", imageService.getProfile().get().string()); + } + + @Test + public void buildManifestWorkWithMetadataTest() throws Exception { + workObj.setSubject(Arrays.asList("Images", "Transformation")); + workObj.setAbstractText("This is a test work"); + workObj.setCreator(Arrays.asList("Boxy", "Boxc")); + workObj.setLanguage(Arrays.asList("English", "Spanish")); + workObj.setParentCollection("Image Collection|" + COLL_ID); + when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList(workObj)); + + var manifest = manifestService.buildManifest(WORK_PID, agent); + assertEquals("http://example.com/iiif/v3/5d72b84a-983c-4a45-8caa-dc9857987da2/manifest", manifest.getID().toString()); + assertEquals("Test Work", manifest.getLabel().getString()); + assertEquals("University of North Carolina Libraries, Digital Collections Repository - Part of Image Collection", + manifest.getRequiredStatement().getValue().getString()); + var abstractMd = manifest.getMetadata().get(0); + assertEquals("description", abstractMd.getLabel().getString()); + assertEquals("This is a test work", abstractMd.getValue().getString()); + + var creatorsMd = manifest.getMetadata().get(1); + assertEquals("Creators", creatorsMd.getLabel().getString()); + assertEquals("Boxy, Boxc", creatorsMd.getValue().getString()); + + var subjectsMd = manifest.getMetadata().get(2); + assertEquals("Subjects", subjectsMd.getLabel().getString()); + assertEquals("Images, Transformation", subjectsMd.getValue().getString()); + + var languagesMd = manifest.getMetadata().get(3); + assertEquals("Languages", languagesMd.getLabel().getString()); + assertEquals("English, Spanish", languagesMd.getValue().getString()); + + var recordLinkMd = manifest.getMetadata().get(4); + assertEquals("", recordLinkMd.getLabel().getString()); + assertEquals("View full record", + recordLinkMd.getValue().getString()); + } +} diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java new file mode 100644 index 0000000000..57d03211da --- /dev/null +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java @@ -0,0 +1,117 @@ +package edu.unc.lib.boxc.web.services.rest; + +import edu.unc.lib.boxc.auth.api.Permission; +import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; +import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; +import edu.unc.lib.boxc.auth.fcrepo.services.GroupsThreadStore; +import edu.unc.lib.boxc.model.api.ResourceType; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; +import edu.unc.lib.boxc.search.solr.models.ContentObjectSolrRecord; +import edu.unc.lib.boxc.web.common.services.AccessCopiesService; +import edu.unc.lib.boxc.web.services.processing.IiifV3ManifestService; +import edu.unc.lib.boxc.web.services.rest.exceptions.RestResponseEntityExceptionHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.openMocks; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author bbpennel + */ +public class IiifV3ManifestControllerTest { + private static final String IIIF_BASE = "http://example.com/iiif/v3/"; + private static final String SERVICES_BASE = "http://example.com/services/"; + private static final String ACCESS_BASE = "http://example.com/"; + + private static final String WORK_ID = "f277bb38-272c-471c-a28a-9887a1328a1f"; + private static final PID WORK_PID = PIDs.get(WORK_ID); + private final static String USERNAME = "test_user"; + private final static AccessGroupSet GROUPS = new AccessGroupSetImpl("adminGroup"); + + @InjectMocks + private IiifV3ManifestController manifestController; + + @Mock + private AccessControlService accessControlService; + + @Mock + private AccessCopiesService accessCopiesService; + + private IiifV3ManifestService manifestService; + + private ContentObjectSolrRecord workObj; + + private MockMvc mockMvc; + + private AutoCloseable closeable; + + @BeforeEach + public void setup() { + closeable = openMocks(this); + manifestService = new IiifV3ManifestService(); + manifestService.setAccessCopiesService(accessCopiesService); + manifestService.setBaseIiifv3Path(IIIF_BASE); + manifestService.setBaseServicesPath(SERVICES_BASE); + manifestService.setBaseAccessPath(ACCESS_BASE); + manifestController.setManifestService(manifestService); + mockMvc = MockMvcBuilders.standaloneSetup(manifestController) + .setControllerAdvice(new RestResponseEntityExceptionHandler()) + .build(); + GroupsThreadStore.storeUsername(USERNAME); + GroupsThreadStore.storeGroups(GROUPS); + + workObj = new ContentObjectSolrRecord(); + workObj.setId(WORK_ID); + workObj.setResourceType(ResourceType.Work.name()); + workObj.setTitle("Test Work"); + } + + @Test + public void testGetManifest() throws Exception { + when(accessCopiesService.listViewableFiles(eq(WORK_PID), any())).thenReturn(Arrays.asList(workObj)); + + var result = mockMvc.perform(get("/iiif/v3/" + WORK_ID + "/manifest") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + Map respMap = MvcTestHelpers.getMapFromResponse(result); + System.out.println(respMap); + assertEquals("Manifest", respMap.get("type")); + assertEquals("http://example.com/iiif/v3/f277bb38-272c-471c-a28a-9887a1328a1f/manifest", respMap.get("id")); + assertEquals("Test Work", ((List) ((Map) respMap.get("label")).get("none")).get(0)); + var metadata = (List) respMap.get("metadata"); + assertFalse(metadata.isEmpty()); + } + + @Test + public void testGetManifestNoAccess() throws Exception { + doThrow(new AccessRestrictionException()).when(accessControlService) + .assertHasAccess(eq(WORK_PID), any(), eq(Permission.viewAccessCopies)); + when(accessCopiesService.listViewableFiles(eq(WORK_PID), any())).thenReturn(Arrays.asList(workObj)); + + mockMvc.perform(get("/iiif/v3/" + WORK_ID + "/manifest") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } +} From 6b5700d3575c22fbee133471ca49d28067eecd7c Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Fri, 3 Nov 2023 09:45:27 -0400 Subject: [PATCH 2/7] Add context config. Update variable name --- .../web/services/processing/IiifV3ManifestService.java | 8 ++++---- .../src/main/webapp/WEB-INF/service-context.xml | 6 ++++++ .../services/processing/IiifV3ManifestServiceTest.java | 4 ++-- .../web/services/rest/IiifV3ManifestControllerTest.java | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java index 63639443a6..4984df70e4 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java @@ -33,7 +33,7 @@ public class IiifV3ManifestService { private AccessCopiesService accessCopiesService; private String baseIiifv3Path; private String baseAccessPath; - private String baseServicesPath; + private String baseServicesApiPath; /** * Constructs a manifest record for the object identified by the provided PID @@ -182,7 +182,7 @@ private String getServicePath(ContentObjectRecord contentObj) { } private String makeThumbnailUrl(String id) { - return URIUtil.join(baseServicesPath, "api", "thumb", id, "large"); + return URIUtil.join(baseServicesApiPath, "thumb", id, "large"); } private boolean hasViewableContent(ContentObjectRecord contentObj) { @@ -202,7 +202,7 @@ public void setBaseAccessPath(String baseAccessPath) { this.baseAccessPath = baseAccessPath; } - public void setBaseServicesPath(String baseServicesPath) { - this.baseServicesPath = baseServicesPath; + public void setBaseServicesApiPath(String baseServicesApiPath) { + this.baseServicesApiPath = baseServicesApiPath; } } diff --git a/web-services-app/src/main/webapp/WEB-INF/service-context.xml b/web-services-app/src/main/webapp/WEB-INF/service-context.xml index 1c4bfa967c..3d5076b27d 100644 --- a/web-services-app/src/main/webapp/WEB-INF/service-context.xml +++ b/web-services-app/src/main/webapp/WEB-INF/service-context.xml @@ -436,6 +436,12 @@ + + + + + + diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java index 6274705181..27d2ef27aa 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java @@ -30,7 +30,7 @@ */ public class IiifV3ManifestServiceTest { private static final String IIIF_BASE = "http://example.com/iiif/v3/"; - private static final String SERVICES_BASE = "http://example.com/services/"; + private static final String SERVICES_BASE = "http://example.com/services/api/"; private static final String ACCESS_BASE = "http://example.com/"; private static final String WORK_ID = "5d72b84a-983c-4a45-8caa-dc9857987da2"; private static final String FILE1_ID = "faffb3e1-85fc-451f-9075-c60fc7584c7b"; @@ -56,7 +56,7 @@ public void setup() { manifestService = new IiifV3ManifestService(); manifestService.setAccessCopiesService(accessCopiesService); manifestService.setBaseIiifv3Path(IIIF_BASE); - manifestService.setBaseServicesPath(SERVICES_BASE); + manifestService.setBaseServicesApiPath(SERVICES_BASE); manifestService.setBaseAccessPath(ACCESS_BASE); when(agent.getPrincipals()).thenReturn(principals); diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java index 57d03211da..d88cc3f9ac 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java @@ -71,7 +71,7 @@ public void setup() { manifestService = new IiifV3ManifestService(); manifestService.setAccessCopiesService(accessCopiesService); manifestService.setBaseIiifv3Path(IIIF_BASE); - manifestService.setBaseServicesPath(SERVICES_BASE); + manifestService.setBaseServicesApiPath(SERVICES_BASE); manifestService.setBaseAccessPath(ACCESS_BASE); manifestController.setManifestService(manifestService); mockMvc = MockMvcBuilders.standaloneSetup(manifestController) From 289c6cf3c50f6be4b02055fc634f1ef098622a17 Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Fri, 3 Nov 2023 09:47:32 -0400 Subject: [PATCH 3/7] Add java version note --- web-services-app/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/web-services-app/pom.xml b/web-services-app/pom.xml index d17371dda8..ac8b7f2410 100644 --- a/web-services-app/pom.xml +++ b/web-services-app/pom.xml @@ -169,6 +169,7 @@ info.freelibrary jiiify-presentation-v3 + 0.12.3 From e9c227181322a69bfffe308de73227bcd6abee62 Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Fri, 3 Nov 2023 12:08:43 -0400 Subject: [PATCH 4/7] Add canvas endpoint --- .../processing/IiifV3ManifestService.java | 34 ++++++++++++- .../rest/IiifV3ManifestController.java | 34 +++++-------- .../processing/IiifV3ManifestServiceTest.java | 38 ++++++++++++-- .../rest/IiifV3ManifestControllerTest.java | 51 +++++++++++++------ 4 files changed, 115 insertions(+), 42 deletions(-) diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java index 4984df70e4..b1c25747a2 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java @@ -1,6 +1,9 @@ package edu.unc.lib.boxc.web.services.processing; +import edu.unc.lib.boxc.auth.api.Permission; import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.api.services.DatastreamPermissionUtil; import edu.unc.lib.boxc.common.util.URIUtil; import edu.unc.lib.boxc.model.api.DatastreamType; import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; @@ -24,6 +27,8 @@ import java.util.ArrayList; import java.util.List; +import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY; + /** * Service for generating iiif v3 manifests for repository object * @author bbpennel @@ -31,6 +36,7 @@ public class IiifV3ManifestService { private static final Logger log = LoggerFactory.getLogger(IiifV3ManifestService.class); private AccessCopiesService accessCopiesService; + private AccessControlService accessControlService; private String baseIiifv3Path; private String baseAccessPath; private String baseServicesApiPath; @@ -42,6 +48,7 @@ public class IiifV3ManifestService { * @return */ public Manifest buildManifest(PID pid, AgentPrincipals agent) { + assertHasAccess(pid, agent); var contentObjs = accessCopiesService.listViewableFiles(pid, agent.getPrincipals()); if (contentObjs.size() == 0) { throw new NotFoundException("No objects were found for inclusion in manifest for object " + pid.getId()); @@ -107,12 +114,25 @@ private void addItems(Manifest manifest, List contentObjs) manifest.setCanvases(canvases); } + /** + * Constructs a standalone canvas document for the requested object + * @param pid + * @param agent + * @return + */ + public Canvas buildCanvas(PID pid, AgentPrincipals agent) { + assertHasAccess(pid, agent); + var contentObjs = accessCopiesService.listViewableFiles(pid, agent.getPrincipals()); + ContentObjectRecord rootObj = contentObjs.get(0); + return createCanvas(rootObj); + } + /** * Constructs a canvas record for the provided object * @param contentObj * @return */ - public Canvas createCanvas(ContentObjectRecord contentObj) { + private Canvas createCanvas(ContentObjectRecord contentObj) { String title = getTitle(contentObj); String uuid = contentObj.getId(); @@ -157,6 +177,14 @@ private void assignDimensions(ContentObjectRecord contentObj, Canvas canvas, Ima } } + private void assertHasAccess(PID pid, AgentPrincipals agent) { + Permission permission = DatastreamPermissionUtil.getPermissionForDatastream(JP2_ACCESS_COPY); + + log.debug("Checking if user {} has access to {} belonging to object {}.", + agent.getUsername(), JP2_ACCESS_COPY, pid); + accessControlService.assertHasAccess(pid, agent.getPrincipals(), permission); + } + private String getManifestPath(ContentObjectRecord contentObj) { return URIUtil.join(baseIiifv3Path, contentObj.getId(), "manifest"); } @@ -194,6 +222,10 @@ public void setAccessCopiesService(AccessCopiesService accessCopiesService) { this.accessCopiesService = accessCopiesService; } + public void setAccessControlService(AccessControlService accessControlService) { + this.accessControlService = accessControlService; + } + public void setBaseIiifv3Path(String baseIiifv3Path) { this.baseIiifv3Path = baseIiifv3Path; } diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java index e33dd9f15c..59c2c7ed9a 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java @@ -1,9 +1,5 @@ package edu.unc.lib.boxc.web.services.rest; -import edu.unc.lib.boxc.auth.api.Permission; -import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; -import edu.unc.lib.boxc.auth.api.services.AccessControlService; -import edu.unc.lib.boxc.auth.api.services.DatastreamPermissionUtil; import edu.unc.lib.boxc.auth.fcrepo.models.AgentPrincipalsImpl; import edu.unc.lib.boxc.model.api.ids.PID; import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; @@ -17,10 +13,8 @@ import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; -import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; /** @@ -29,13 +23,9 @@ * @author bbpennel */ @Controller -@RequestMapping("/iiif/v3/") public class IiifV3ManifestController { private static final Logger log = LoggerFactory.getLogger(IiifV3ManifestController.class); - @Autowired - private AccessControlService accessControlService; - @Autowired private IiifV3ManifestService manifestService; @@ -45,24 +35,26 @@ public class IiifV3ManifestController { * @return Response containing the manifest */ @CrossOrigin - @GetMapping(value = "/{id}/manifest", produces = APPLICATION_JSON_VALUE) + @GetMapping(value = "/iiif/v3/{id}/manifest", produces = APPLICATION_JSON_VALUE) @ResponseBody public ResponseEntity getManifest(@PathVariable("id") String id) { PID pid = PIDs.get(id); - assertHasAccess(pid); - var manifest = manifestService.buildManifest(pid, AgentPrincipalsImpl.createFromThread()); - return new ResponseEntity<>(manifest, HttpStatus.OK); } - private void assertHasAccess(PID pid) { - Permission permission = DatastreamPermissionUtil.getPermissionForDatastream(JP2_ACCESS_COPY); - - AgentPrincipals agent = AgentPrincipalsImpl.createFromThread(); - log.debug("Checking if user {} has access to {} belonging to object {}.", - agent.getUsername(), JP2_ACCESS_COPY, pid); - accessControlService.assertHasAccess(pid, agent.getPrincipals(), permission); + /** + * Handles requests for IIIF v3 manifests + * @param id + * @return Response containing the manifest + */ + @CrossOrigin + @GetMapping(value = "/iiif/v3/{id}/canvas", produces = APPLICATION_JSON_VALUE) + @ResponseBody + public ResponseEntity getCanvas(@PathVariable("id") String id) { + PID pid = PIDs.get(id); + var manifest = manifestService.buildCanvas(pid, AgentPrincipalsImpl.createFromThread()); + return new ResponseEntity<>(manifest, HttpStatus.OK); } public void setManifestService(IiifV3ManifestService manifestService) { diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java index 27d2ef27aa..69f1683083 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java @@ -1,7 +1,10 @@ package edu.unc.lib.boxc.web.services.processing; +import edu.unc.lib.boxc.auth.api.Permission; +import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; import edu.unc.lib.boxc.model.api.ResourceType; import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; import edu.unc.lib.boxc.model.api.ids.PID; @@ -22,6 +25,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; @@ -41,6 +47,8 @@ public class IiifV3ManifestServiceTest { @Mock private AccessCopiesService accessCopiesService; @Mock + private AccessControlService accessControlService; + @Mock private AgentPrincipals agent; @Mock private AccessGroupSet principals; @@ -55,6 +63,7 @@ public void setup() { closeable = openMocks(this); manifestService = new IiifV3ManifestService(); manifestService.setAccessCopiesService(accessCopiesService); + manifestService.setAccessControlService(accessControlService); manifestService.setBaseIiifv3Path(IIIF_BASE); manifestService.setBaseServicesApiPath(SERVICES_BASE); manifestService.setBaseAccessPath(ACCESS_BASE); @@ -79,7 +88,7 @@ private ContentObjectRecord createFileRecord(String id) { } @Test - public void buildManifestNoViewableTest() throws Exception { + public void buildManifestNoViewableTest() { when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList()); assertThrows(NotFoundException.class, () -> { manifestService.buildManifest(WORK_PID, agent); @@ -87,7 +96,16 @@ public void buildManifestNoViewableTest() throws Exception { } @Test - public void buildManifestWorkWithoutViewableTest() throws Exception { + public void buildManifestNoAccessTest() { + doThrow(new AccessRestrictionException()).when(accessControlService) + .assertHasAccess(eq(WORK_PID), any(), eq(Permission.viewAccessCopies)); + assertThrows(AccessRestrictionException.class, () -> { + manifestService.buildManifest(WORK_PID, agent); + }); + } + + @Test + public void buildManifestWorkWithoutViewableTest() { when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList(workObj)); var manifest = manifestService.buildManifest(WORK_PID, agent); @@ -99,7 +117,7 @@ public void buildManifestWorkWithoutViewableTest() throws Exception { } @Test - public void buildManifestWorkWithViewableFilesTest() throws Exception { + public void buildManifestWorkWithViewableFilesTest() { var fileObj1 = createFileRecord(FILE1_ID); var fileObj2 = createFileRecord(FILE2_ID); when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList(workObj, fileObj1, fileObj2)); @@ -115,7 +133,7 @@ public void buildManifestWorkWithViewableFilesTest() throws Exception { } @Test - public void buildManifestViewableFileTest() throws Exception { + public void buildManifestViewableFileTest() { var fileObj1 = createFileRecord(FILE1_ID); var filePid = PIDs.get(FILE1_ID); when(accessCopiesService.listViewableFiles(filePid, principals)).thenReturn(Arrays.asList(fileObj1)); @@ -131,6 +149,16 @@ public void buildManifestViewableFileTest() throws Exception { assertEquals("View full record", manifest.getMetadata().get(0).getValue().getString()); } + + @Test + public void buildCanvasViewableFileTest() { + var fileObj1 = createFileRecord(FILE1_ID); + var filePid = PIDs.get(FILE1_ID); + when(accessCopiesService.listViewableFiles(filePid, principals)).thenReturn(Arrays.asList(fileObj1)); + + var canvas = manifestService.buildCanvas(filePid, agent); + assertFileCanvasPopulated(canvas, FILE1_ID); + } private void assertFileCanvasPopulated(Canvas fileCanvas, String expectedId) { assertEquals("http://example.com/iiif/v3/" + expectedId + "/canvas", @@ -153,7 +181,7 @@ private void assertFileCanvasPopulated(Canvas fileCanvas, String expectedId) { } @Test - public void buildManifestWorkWithMetadataTest() throws Exception { + public void buildManifestWorkWithMetadataTest() { workObj.setSubject(Arrays.asList("Images", "Transformation")); workObj.setAbstractText("This is a test work"); workObj.setCreator(Arrays.asList("Boxy", "Boxc")); diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java index d88cc3f9ac..a0eb750b1a 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java @@ -10,6 +10,7 @@ import edu.unc.lib.boxc.model.api.ids.PID; import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; import edu.unc.lib.boxc.search.solr.models.ContentObjectSolrRecord; +import edu.unc.lib.boxc.search.solr.models.DatastreamImpl; import edu.unc.lib.boxc.web.common.services.AccessCopiesService; import edu.unc.lib.boxc.web.services.processing.IiifV3ManifestService; import edu.unc.lib.boxc.web.services.rest.exceptions.RestResponseEntityExceptionHandler; @@ -43,8 +44,8 @@ public class IiifV3ManifestControllerTest { private static final String SERVICES_BASE = "http://example.com/services/"; private static final String ACCESS_BASE = "http://example.com/"; - private static final String WORK_ID = "f277bb38-272c-471c-a28a-9887a1328a1f"; - private static final PID WORK_PID = PIDs.get(WORK_ID); + private static final String OBJECT_ID = "f277bb38-272c-471c-a28a-9887a1328a1f"; + private static final PID OBJECT_PID = PIDs.get(OBJECT_ID); private final static String USERNAME = "test_user"; private final static AccessGroupSet GROUPS = new AccessGroupSetImpl("adminGroup"); @@ -59,8 +60,6 @@ public class IiifV3ManifestControllerTest { private IiifV3ManifestService manifestService; - private ContentObjectSolrRecord workObj; - private MockMvc mockMvc; private AutoCloseable closeable; @@ -70,6 +69,7 @@ public void setup() { closeable = openMocks(this); manifestService = new IiifV3ManifestService(); manifestService.setAccessCopiesService(accessCopiesService); + manifestService.setAccessControlService(accessControlService); manifestService.setBaseIiifv3Path(IIIF_BASE); manifestService.setBaseServicesApiPath(SERVICES_BASE); manifestService.setBaseAccessPath(ACCESS_BASE); @@ -79,24 +79,22 @@ public void setup() { .build(); GroupsThreadStore.storeUsername(USERNAME); GroupsThreadStore.storeGroups(GROUPS); - - workObj = new ContentObjectSolrRecord(); - workObj.setId(WORK_ID); - workObj.setResourceType(ResourceType.Work.name()); - workObj.setTitle("Test Work"); } @Test public void testGetManifest() throws Exception { - when(accessCopiesService.listViewableFiles(eq(WORK_PID), any())).thenReturn(Arrays.asList(workObj)); + var workObj = new ContentObjectSolrRecord(); + workObj.setId(OBJECT_ID); + workObj.setResourceType(ResourceType.Work.name()); + workObj.setTitle("Test Work"); + when(accessCopiesService.listViewableFiles(eq(OBJECT_PID), any())).thenReturn(Arrays.asList(workObj)); - var result = mockMvc.perform(get("/iiif/v3/" + WORK_ID + "/manifest") + var result = mockMvc.perform(get("/iiif/v3/" + OBJECT_ID + "/manifest") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); Map respMap = MvcTestHelpers.getMapFromResponse(result); - System.out.println(respMap); assertEquals("Manifest", respMap.get("type")); assertEquals("http://example.com/iiif/v3/f277bb38-272c-471c-a28a-9887a1328a1f/manifest", respMap.get("id")); assertEquals("Test Work", ((List) ((Map) respMap.get("label")).get("none")).get(0)); @@ -107,11 +105,34 @@ public void testGetManifest() throws Exception { @Test public void testGetManifestNoAccess() throws Exception { doThrow(new AccessRestrictionException()).when(accessControlService) - .assertHasAccess(eq(WORK_PID), any(), eq(Permission.viewAccessCopies)); - when(accessCopiesService.listViewableFiles(eq(WORK_PID), any())).thenReturn(Arrays.asList(workObj)); + .assertHasAccess(eq(OBJECT_PID), any(), eq(Permission.viewAccessCopies)); - mockMvc.perform(get("/iiif/v3/" + WORK_ID + "/manifest") + mockMvc.perform(get("/iiif/v3/" + OBJECT_ID + "/manifest") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isForbidden()); } + + @Test + public void testGetCanvas() throws Exception { + var fileObj = new ContentObjectSolrRecord(); + fileObj.setId(OBJECT_ID); + fileObj.setResourceType(ResourceType.File.name()); + fileObj.setTitle("File Object"); + var originalDs = new DatastreamImpl("original_file|image/jpeg|image.jpg|jpg|0|||240x750"); + var jp2Ds = new DatastreamImpl("jp2|image/jp2|image.jp2|jp2|0|||"); + fileObj.setDatastream(Arrays.asList(originalDs.toString(), jp2Ds.toString())); + when(accessCopiesService.listViewableFiles(eq(OBJECT_PID), any())).thenReturn(Arrays.asList(fileObj)); + + var result = mockMvc.perform(get("/iiif/v3/" + OBJECT_ID + "/canvas") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + Map respMap = MvcTestHelpers.getMapFromResponse(result); + assertEquals("Canvas", respMap.get("type")); + assertEquals("http://example.com/iiif/v3/f277bb38-272c-471c-a28a-9887a1328a1f/canvas", respMap.get("id")); + assertEquals(750, respMap.get("width")); + var items = (List) respMap.get("items"); + assertFalse(items.isEmpty()); + } } From f6a59dbf0059a24cdf137ec6d4f2c99e1fc08bcd Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Mon, 6 Nov 2023 08:21:02 -0500 Subject: [PATCH 5/7] Add missing dependencies --- .../boxc/web/services/processing/IiifV3ManifestService.java | 3 ++- web-services-app/src/main/webapp/WEB-INF/service-context.xml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java index b1c25747a2..6fb130803a 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java @@ -6,6 +6,7 @@ import edu.unc.lib.boxc.auth.api.services.DatastreamPermissionUtil; import edu.unc.lib.boxc.common.util.URIUtil; import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.api.ResourceType; import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; import edu.unc.lib.boxc.model.api.ids.PID; import edu.unc.lib.boxc.search.api.models.ContentObjectRecord; @@ -215,7 +216,7 @@ private String makeThumbnailUrl(String id) { private boolean hasViewableContent(ContentObjectRecord contentObj) { var datastream = contentObj.getDatastreamObject(DatastreamType.JP2_ACCESS_COPY.getId()); - return datastream != null; + return datastream != null && contentObj.getResourceType().equals(ResourceType.File.name()); } public void setAccessCopiesService(AccessCopiesService accessCopiesService) { diff --git a/web-services-app/src/main/webapp/WEB-INF/service-context.xml b/web-services-app/src/main/webapp/WEB-INF/service-context.xml index 3d5076b27d..c33a2981d5 100644 --- a/web-services-app/src/main/webapp/WEB-INF/service-context.xml +++ b/web-services-app/src/main/webapp/WEB-INF/service-context.xml @@ -441,6 +441,8 @@ + + From 5248910dd1d626c4ac0b76c9c59e072f9df3ab8b Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Mon, 6 Nov 2023 16:33:07 -0500 Subject: [PATCH 6/7] Address feedback. Rename methods for clarity. Add missing close statements --- .../processing/IiifV3ManifestService.java | 22 +++++++++---------- .../processing/IiifV3ManifestServiceTest.java | 8 ++++++- .../rest/IiifV3ManifestControllerTest.java | 6 +++++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java index 6fb130803a..fc14a5f213 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java @@ -57,29 +57,29 @@ public Manifest buildManifest(PID pid, AgentPrincipals agent) { log.debug("Constructing manifest for {} containing {} items", pid.getId(), contentObjs.size()); ContentObjectRecord rootObj = contentObjs.get(0); var manifest = new Manifest(getManifestPath(rootObj), new Label(getTitle(rootObj))); - manifest.setMetadata(buildMetadata(rootObj)); + manifest.setMetadata(constructMetadataSection(rootObj)); addAttribution(manifest, rootObj); - addItems(manifest, contentObjs); + addCanvasItems(manifest, contentObjs); return manifest; } - private List buildMetadata(ContentObjectRecord rootObj) { + private List constructMetadataSection(ContentObjectRecord rootObj) { var metadataList = new ArrayList(); String abstractText = rootObj.getAbstractText(); if (abstractText != null) { metadataList.add(new Metadata("description", abstractText)); } - addMetadataField(metadataList, "Creators", rootObj.getCreator()); - addMetadataField(metadataList, "Subjects", rootObj.getSubject()); - addMetadataField(metadataList, "Languages", rootObj.getLanguage()); + addMultiValuedMetadataField(metadataList, "Creators", rootObj.getCreator()); + addMultiValuedMetadataField(metadataList, "Subjects", rootObj.getSubject()); + addMultiValuedMetadataField(metadataList, "Languages", rootObj.getLanguage()); metadataList.add(new Metadata("", "View full record")); return metadataList; } - private void addMetadataField(List metadataList, String fieldName, List values) { + private void addMultiValuedMetadataField(List metadataList, String fieldName, List values) { if (!CollectionUtils.isEmpty(values)) { metadataList.add(new Metadata(fieldName, String.join(", ", values))); } @@ -104,12 +104,12 @@ private void addAttribution(Manifest manifest, ContentObjectRecord rootObj) { * @param manifest * @param contentObjs */ - private void addItems(Manifest manifest, List contentObjs) { + private void addCanvasItems(Manifest manifest, List contentObjs) { var canvases = new ArrayList(); for (ContentObjectRecord contentObj : contentObjs) { // Add canvases for any records with displayable content if (hasViewableContent(contentObj)) { - canvases.add(createCanvas(contentObj)); + canvases.add(constructCanvasSection(contentObj)); } } manifest.setCanvases(canvases); @@ -125,7 +125,7 @@ public Canvas buildCanvas(PID pid, AgentPrincipals agent) { assertHasAccess(pid, agent); var contentObjs = accessCopiesService.listViewableFiles(pid, agent.getPrincipals()); ContentObjectRecord rootObj = contentObjs.get(0); - return createCanvas(rootObj); + return constructCanvasSection(rootObj); } /** @@ -133,7 +133,7 @@ public Canvas buildCanvas(PID pid, AgentPrincipals agent) { * @param contentObj * @return */ - private Canvas createCanvas(ContentObjectRecord contentObj) { + private Canvas constructCanvasSection(ContentObjectRecord contentObj) { String title = getTitle(contentObj); String uuid = contentObj.getId(); diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java index 69f1683083..69a646a1db 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java @@ -16,6 +16,7 @@ import info.freelibrary.iiif.presentation.v3.Canvas; import info.freelibrary.iiif.presentation.v3.ImageContent; import info.freelibrary.iiif.presentation.v3.services.ImageService3; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -76,6 +77,11 @@ public void setup() { workObj.setTitle("Test Work"); } + @AfterEach + void closeService() throws Exception { + closeable.close(); + } + private ContentObjectRecord createFileRecord(String id) { var fileObj = new ContentObjectSolrRecord(); fileObj.setId(id); @@ -105,7 +111,7 @@ public void buildManifestNoAccessTest() { } @Test - public void buildManifestWorkWithoutViewableTest() { + public void buildManifestWorkWithoutViewableFilesTest() { when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList(workObj)); var manifest = manifestService.buildManifest(WORK_PID, agent); diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java index a0eb750b1a..76b2111e4d 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java @@ -14,6 +14,7 @@ import edu.unc.lib.boxc.web.common.services.AccessCopiesService; import edu.unc.lib.boxc.web.services.processing.IiifV3ManifestService; import edu.unc.lib.boxc.web.services.rest.exceptions.RestResponseEntityExceptionHandler; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -81,6 +82,11 @@ public void setup() { GroupsThreadStore.storeGroups(GROUPS); } + @AfterEach + void closeService() throws Exception { + closeable.close(); + } + @Test public void testGetManifest() throws Exception { var workObj = new ContentObjectSolrRecord(); From b62a1190c45e28ffce874d25a877c92b2dd36387 Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Mon, 6 Nov 2023 16:37:04 -0500 Subject: [PATCH 7/7] Update method name --- .../boxc/web/services/processing/IiifV3ManifestServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java index 69a646a1db..6e090003c4 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java @@ -94,7 +94,7 @@ private ContentObjectRecord createFileRecord(String id) { } @Test - public void buildManifestNoViewableTest() { + public void buildManifestNoViewableFilesTest() { when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList()); assertThrows(NotFoundException.class, () -> { manifestService.buildManifest(WORK_PID, agent);