diff --git a/web-services-app/pom.xml b/web-services-app/pom.xml index 35e44b0794..ac8b7f2410 100644 --- a/web-services-app/pom.xml +++ b/web-services-app/pom.xml @@ -166,6 +166,12 @@ 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..fc14a5f213 --- /dev/null +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestService.java @@ -0,0 +1,241 @@ +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.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; +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; + +import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY; + +/** + * 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 AccessControlService accessControlService; + private String baseIiifv3Path; + private String baseAccessPath; + private String baseServicesApiPath; + + /** + * Constructs a manifest record for the object identified by the provided PID + * @param pid + * @param agent + * @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()); + } + 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(constructMetadataSection(rootObj)); + addAttribution(manifest, rootObj); + + addCanvasItems(manifest, contentObjs); + + return manifest; + } + + private List constructMetadataSection(ContentObjectRecord rootObj) { + var metadataList = new ArrayList(); + String abstractText = rootObj.getAbstractText(); + if (abstractText != null) { + metadataList.add(new Metadata("description", abstractText)); + } + 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 addMultiValuedMetadataField(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 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(constructCanvasSection(contentObj)); + } + } + 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 constructCanvasSection(rootObj); + } + + /** + * Constructs a canvas record for the provided object + * @param contentObj + * @return + */ + private Canvas constructCanvasSection(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 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"); + } + + 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(baseServicesApiPath, "thumb", id, "large"); + } + + private boolean hasViewableContent(ContentObjectRecord contentObj) { + var datastream = contentObj.getDatastreamObject(DatastreamType.JP2_ACCESS_COPY.getId()); + return datastream != null && contentObj.getResourceType().equals(ResourceType.File.name()); + } + + public void setAccessCopiesService(AccessCopiesService accessCopiesService) { + this.accessCopiesService = accessCopiesService; + } + + public void setAccessControlService(AccessControlService accessControlService) { + this.accessControlService = accessControlService; + } + + public void setBaseIiifv3Path(String baseIiifv3Path) { + this.baseIiifv3Path = baseIiifv3Path; + } + + public void setBaseAccessPath(String baseAccessPath) { + this.baseAccessPath = baseAccessPath; + } + + public void setBaseServicesApiPath(String baseServicesApiPath) { + this.baseServicesApiPath = baseServicesApiPath; + } +} 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..59c2c7ed9a --- /dev/null +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestController.java @@ -0,0 +1,63 @@ +package edu.unc.lib.boxc.web.services.rest; + +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.ResponseBody; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +/** + * Controller which handles iiif v3 requests + * + * @author bbpennel + */ +@Controller +public class IiifV3ManifestController { + private static final Logger log = LoggerFactory.getLogger(IiifV3ManifestController.class); + + @Autowired + private IiifV3ManifestService manifestService; + + /** + * Handles requests for IIIF v3 manifests + * @param id + * @return Response containing the manifest + */ + @CrossOrigin + @GetMapping(value = "/iiif/v3/{id}/manifest", produces = APPLICATION_JSON_VALUE) + @ResponseBody + public ResponseEntity getManifest(@PathVariable("id") String id) { + PID pid = PIDs.get(id); + var manifest = manifestService.buildManifest(pid, AgentPrincipalsImpl.createFromThread()); + return new ResponseEntity<>(manifest, HttpStatus.OK); + } + + /** + * 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) { + this.manifestService = manifestService; + } +} 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..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 @@ -436,6 +436,14 @@ + + + + + + + + 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..6e090003c4 --- /dev/null +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/processing/IiifV3ManifestServiceTest.java @@ -0,0 +1,224 @@ +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; +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.AfterEach; +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.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; + +/** + * @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/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"; + 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 AccessControlService accessControlService; + @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.setAccessControlService(accessControlService); + manifestService.setBaseIiifv3Path(IIIF_BASE); + manifestService.setBaseServicesApiPath(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"); + } + + @AfterEach + void closeService() throws Exception { + closeable.close(); + } + + 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 buildManifestNoViewableFilesTest() { + when(accessCopiesService.listViewableFiles(WORK_PID, principals)).thenReturn(Arrays.asList()); + assertThrows(NotFoundException.class, () -> { + manifestService.buildManifest(WORK_PID, agent); + }); + } + + @Test + 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 buildManifestWorkWithoutViewableFilesTest() { + 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() { + 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() { + 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()); + } + + @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", + 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() { + 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..76b2111e4d --- /dev/null +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/IiifV3ManifestControllerTest.java @@ -0,0 +1,144 @@ +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.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; +import org.junit.jupiter.api.AfterEach; +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 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"); + + @InjectMocks + private IiifV3ManifestController manifestController; + + @Mock + private AccessControlService accessControlService; + + @Mock + private AccessCopiesService accessCopiesService; + + private IiifV3ManifestService manifestService; + + private MockMvc mockMvc; + + private AutoCloseable closeable; + + @BeforeEach + 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); + manifestController.setManifestService(manifestService); + mockMvc = MockMvcBuilders.standaloneSetup(manifestController) + .setControllerAdvice(new RestResponseEntityExceptionHandler()) + .build(); + GroupsThreadStore.storeUsername(USERNAME); + GroupsThreadStore.storeGroups(GROUPS); + } + + @AfterEach + void closeService() throws Exception { + closeable.close(); + } + + @Test + public void testGetManifest() throws Exception { + 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/" + OBJECT_ID + "/manifest") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + Map respMap = MvcTestHelpers.getMapFromResponse(result); + 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(OBJECT_PID), any(), eq(Permission.viewAccessCopies)); + + 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()); + } +}