diff --git a/integration/src/test/java/edu/unc/lib/boxc/integration/web/common/LorisContentIT.java b/integration/src/test/java/edu/unc/lib/boxc/integration/web/common/LorisContentIT.java deleted file mode 100644 index e01b50c990..0000000000 --- a/integration/src/test/java/edu/unc/lib/boxc/integration/web/common/LorisContentIT.java +++ /dev/null @@ -1,344 +0,0 @@ -package edu.unc.lib.boxc.integration.web.common; - -import static edu.unc.lib.boxc.model.api.DatastreamType.TECHNICAL_METADATA; -import static edu.unc.lib.boxc.model.api.xml.NamespaceConstants.FITS_URI; -import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.getContentRootPid; -import static edu.unc.lib.boxc.auth.api.AccessPrincipalConstants.AUTHENTICATED_PRINC; -import static edu.unc.lib.boxc.auth.api.AccessPrincipalConstants.PUBLIC_PRINC; -import static org.apache.jena.rdf.model.ResourceFactory.createResource; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; - -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.io.FileUtils; -import org.apache.jena.rdf.model.Model; -import org.apache.jena.vocabulary.DCTerms; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.ContextHierarchy; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import de.digitalcollections.iiif.model.jackson.IiifObjectMapper; -import de.digitalcollections.iiif.model.openannotation.Annotation; -import de.digitalcollections.iiif.model.sharedcanvas.Canvas; -import de.digitalcollections.iiif.model.sharedcanvas.Manifest; -import de.digitalcollections.iiif.model.sharedcanvas.Sequence; -import edu.unc.lib.boxc.model.api.DatastreamType; -import edu.unc.lib.boxc.model.api.ids.PID; -import edu.unc.lib.boxc.model.api.ids.PIDMinter; -import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; -import edu.unc.lib.boxc.model.api.rdf.IanaRelation; -import edu.unc.lib.boxc.model.api.services.RepositoryObjectFactory; -import edu.unc.lib.boxc.model.fcrepo.ids.DatastreamPids; -import edu.unc.lib.boxc.model.api.objects.AdminUnit; -import edu.unc.lib.boxc.model.api.objects.CollectionObject; -import edu.unc.lib.boxc.model.api.objects.ContentRootObject; -import edu.unc.lib.boxc.model.api.objects.FileObject; -import edu.unc.lib.boxc.model.api.objects.WorkObject; -import edu.unc.lib.boxc.model.fcrepo.services.DerivativeService; -import edu.unc.lib.boxc.model.fcrepo.services.RepositoryInitializer; -import edu.unc.lib.boxc.model.fcrepo.test.AclModelBuilder; -import edu.unc.lib.boxc.model.fcrepo.test.RepositoryObjectTreeIndexer; -import edu.unc.lib.boxc.model.fcrepo.test.TestHelper; -import edu.unc.lib.boxc.auth.api.services.AccessControlService; -import edu.unc.lib.boxc.auth.api.services.GlobalPermissionEvaluator; -import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; -import edu.unc.lib.boxc.auth.fcrepo.services.GroupsThreadStore; -import edu.unc.lib.boxc.indexing.solr.test.RepositoryObjectSolrIndexer; -import edu.unc.lib.boxc.auth.api.UserRole; - -/** - * @author bbpennel - */ -@ContextHierarchy({ - @ContextConfiguration("/spring-test/test-fedora-container.xml"), - @ContextConfiguration("/spring-test/cdr-client-container.xml"), - @ContextConfiguration("/spring-test/acl-service-context.xml"), - @ContextConfiguration("/spring-test/solr-embedded-context.xml"), - @ContextConfiguration("/spring-test/solr-indexing-context.xml"), - @ContextConfiguration("/loris-content-it-servlet.xml") -}) -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -public class LorisContentIT { - - @Autowired - protected String baseAddress; - @Autowired - protected WebApplicationContext context; - @Autowired - protected AccessControlService aclService; - @Autowired - protected GlobalPermissionEvaluator globalPermEvaluator; - @Autowired - protected RepositoryObjectFactory repoObjFactory; - @Autowired - protected RepositoryObjectLoader repositoryObjectLoader; - @Autowired - protected PIDMinter pidMinter; - @Autowired - protected RepositoryObjectTreeIndexer treeIndexer; - @Autowired - private RepositoryObjectSolrIndexer solrIndexer; - @Autowired - protected RepositoryInitializer repoInitializer; - @Autowired - private DerivativeService derivativeService; - - protected ContentRootObject contentRoot; - - protected MockMvc mvc; - - protected AdminUnit unitObj; - protected CollectionObject collObj; - - @BeforeEach - public void setup() throws Exception { - - mvc = MockMvcBuilders - .webAppContextSetup(context) - .build(); - - TestHelper.setContentBase("http://localhost:48085/rest"); - - GroupsThreadStore.storeUsername("test_user"); - GroupsThreadStore.storeGroups(new AccessGroupSetImpl("adminGroup")); - - generateBaseStructure(); - } - - @AfterEach - public void tearDown() { - GroupsThreadStore.clearStore(); - } - - @Test - public void testGetManifestFileWithJp2() throws Exception { - WorkObject workObj = repoObjFactory.createWorkObject(new AclModelBuilder("Work").model); - collObj.addMember(workObj); - FileObject fileObj = addFileObject(workObj, true); - - treeIndexer.indexAll(baseAddress); - solrIndexer.index(contentRoot.getPid(), unitObj.getPid(), collObj.getPid(), - workObj.getPid(), fileObj.getPid()); - - // Both the file and the work should return an image manifest - assertHasImageManifest(fileObj.getPid(), fileObj.getPid(), "file", "file"); - assertHasImageManifest(workObj.getPid(), fileObj.getPid(), "Work", "file"); - - // Should produce the same result with a primary object - workObj.setPrimaryObject(fileObj.getPid()); - treeIndexer.indexAll(workObj.getPid().getRepositoryPath()); - solrIndexer.index(workObj.getPid(), fileObj.getPid()); - assertHasImageManifest(workObj.getPid(), fileObj.getPid(), "Work", "file"); - } - - @Test - public void testGetManifestNoImage() throws Exception { - WorkObject workObj = repoObjFactory.createWorkObject(new AclModelBuilder("Work3").model); - collObj.addMember(workObj); - FileObject fileObj = addFileObject(workObj, false); - - treeIndexer.indexAll(baseAddress); - solrIndexer.index(contentRoot.getPid(), unitObj.getPid(), collObj.getPid(), - workObj.getPid(), fileObj.getPid()); - - // Neither object should return a manifest - assertNoManifest(fileObj.getPid()); - assertEmptyManifest(workObj.getPid(), "Work3"); - } - - @Test - public void testGetManifestCollection() throws Exception { - treeIndexer.indexAll(baseAddress); - solrIndexer.index(contentRoot.getPid(), unitObj.getPid(), collObj.getPid()); - - assertNoManifest(collObj.getPid()); - } - - @Test - public void testGetManifestMultipleFiles() throws Exception { - WorkObject workObj = repoObjFactory.createWorkObject(null); - collObj.addMember(workObj); - FileObject fileObj = addFileObject(workObj, true); - FileObject fileObj2 = addFileObject(workObj, "file2.txt", false, null); - FileObject fileObj3 = addFileObject(workObj, "file3.png", true, null); - - treeIndexer.indexAll(baseAddress); - solrIndexer.index(contentRoot.getPid(), unitObj.getPid(), collObj.getPid(), - workObj.getPid(), fileObj.getPid(), fileObj2.getPid(), fileObj3.getPid()); - - Manifest manifest = callGetManifest(workObj.getPid()); - assertEquals(2, manifest.getSequences().get(0).getCanvases().size()); - assertManifestContainsImage(manifest, 0, fileObj.getPid(), "file"); - assertManifestContainsImage(manifest, 1, fileObj3.getPid(), "file3.png"); - - workObj.setPrimaryObject(fileObj.getPid()); - treeIndexer.indexAll(baseAddress); - solrIndexer.index(fileObj.getPid(), fileObj2.getPid(), fileObj3.getPid()); - } - - @Test - public void testGetManifestPrimaryObjectNonImage() throws Exception { - WorkObject workObj = repoObjFactory.createWorkObject(new AclModelBuilder("Work4").model); - collObj.addMember(workObj); - FileObject fileObj = addFileObject(workObj, "file2.txt", false, null); - FileObject fileObj2 = addFileObject(workObj, "file3.png", true, null); - workObj.setPrimaryObject(fileObj.getPid()); - - treeIndexer.indexAll(baseAddress); - solrIndexer.index(contentRoot.getPid(), unitObj.getPid(), collObj.getPid(), - workObj.getPid(), fileObj.getPid(), fileObj2.getPid()); - - assertHasImageManifest(workObj.getPid(), fileObj2.getPid(), "Work4", "file3.png"); - } - - @Test - public void testGetManifestJp2MetadataOnly() throws Exception { - GroupsThreadStore.storeGroups(new AccessGroupSetImpl(PUBLIC_PRINC)); - - WorkObject workObj = repoObjFactory.createWorkObject(new AclModelBuilder("Work2").model); - collObj.addMember(workObj); - FileObject fileObj = addFileObject(workObj, "file", true, UserRole.canViewMetadata); - FileObject fileObj2 = addFileObject(workObj, "file2", true, null); - - treeIndexer.indexAll(baseAddress); - solrIndexer.index(contentRoot.getPid(), unitObj.getPid(), collObj.getPid(), - workObj.getPid(), fileObj.getPid(), fileObj2.getPid()); - - assertHasImageManifest(workObj.getPid(), fileObj2.getPid(), "Work2", "file2"); - assertHasImageManifest(fileObj2.getPid(), fileObj2.getPid(), "file2", "file2"); - assertNoAccess(fileObj.getPid()); - } - - private void assertNoManifest(PID pid) throws Exception { - mvc.perform(get("/jp2Proxy/" + pid.getId() + "/jp2/manifest")) - .andExpect(status().isNotFound()) - .andReturn(); - } - - private void assertNoAccess(PID pid) throws Exception { - mvc.perform(get("/jp2Proxy/" + pid.getId() + "/jp2/manifest")) - .andExpect(status().isForbidden()) - .andReturn(); - } - - private Manifest callGetManifest(PID pid) throws Exception { - MvcResult result = mvc.perform(get("/jp2Proxy/" + pid.getId() + "/jp2/manifest")) - .andExpect(status().isOk()) - .andReturn(); - - return parseManifestResponse(result); - } - - private void assertEmptyManifest(PID pid, String label) throws Exception { - Manifest manifest = callGetManifest(pid); - Sequence sequence = manifest.getSequences().get(0); - assertTrue(CollectionUtils.isEmpty(sequence.getCanvases())); - assertEquals(label, manifest.getLabelString()); - } - - private void assertHasImageManifest(PID pid, PID filePid, String label, String fileLabel) throws Exception { - Manifest manifest = callGetManifest(pid); - assertEquals(label, manifest.getLabelString()); - Sequence sequence = manifest.getSequences().get(0); - List canvases = sequence.getCanvases(); - assertEquals(1, canvases.size()); - assertManifestContainsImage(manifest, 0, filePid, fileLabel); - } - - private void assertManifestContainsImage(Manifest manifest, int index, PID filePid, String label) { - Sequence sequence = manifest.getSequences().get(0); - List canvases = sequence.getCanvases(); - Canvas canvas = canvases.get(index); - assertEquals(label, canvas.getLabelString()); - assertEquals(375, canvas.getHeight().intValue()); - assertEquals(250, canvas.getWidth().intValue()); - List images = canvas.getImages(); - assertEquals(2, images.size()); - Annotation jp2Image = images.get(0); - assertEquals("http://localhost:48085/jp2Proxy/" + filePid.getId() + "/jp2", - jp2Image.getResource().getServices().get(0).getIdentifier().toString()); - Annotation thumbImage = images.get(1); - assertEquals("http://localhost:48085/services/api/thumb/" + filePid.getId() + "/large", - thumbImage.getResource().getIdentifier().toString()); - } - - private Manifest parseManifestResponse(MvcResult result) throws Exception { - MockHttpServletResponse response = result.getResponse(); - Manifest manifest = new IiifObjectMapper().readValue(response.getContentAsString(), Manifest.class); - return manifest; - } - - private FileObject addFileObject(WorkObject workObj, boolean isImage) throws Exception { - return addFileObject(workObj, "file", isImage, null); - } - - private FileObject addFileObject(WorkObject workObj, String filename, boolean isImage, UserRole role) - throws Exception { - String bodyString = "Content"; - String mimetype = isImage ? "image/png" : "text/plain"; - Path contentPath = Files.createTempFile("file", ".txt"); - FileUtils.writeStringToFile(contentPath.toFile(), bodyString, "UTF-8"); - - Model model = null; - if (UserRole.canViewMetadata.equals(role)) { - model = new AclModelBuilder(filename).addCanViewMetadata(PUBLIC_PRINC).model; - } else if (UserRole.none.equals(role)) { - model = new AclModelBuilder(filename).addNoneRole(PUBLIC_PRINC).model; - } - - FileObject fileObj = repoObjFactory.createFileObject(model); - fileObj.addOriginalFile(contentPath.toUri(), filename, mimetype, null, null); - PID filePid = fileObj.getPid(); - - if (isImage) { - PID fitsPid = DatastreamPids.getTechnicalMetadataPid(filePid); - fileObj.addBinary(fitsPid, this.getClass().getResource("/datastream/techmd_image.xml").toURI(), - TECHNICAL_METADATA.getDefaultFilename(), TECHNICAL_METADATA.getMimetype(), - null, null, IanaRelation.derivedfrom, DCTerms.conformsTo, createResource(FITS_URI)); - Path jp2Path = derivativeService.getDerivativePath(filePid, DatastreamType.JP2_ACCESS_COPY); - Files.createDirectories(jp2Path.getParent()); - Files.createFile(jp2Path); - } - - workObj.addMember(fileObj); - return fileObj; - } - - private void generateBaseStructure() throws Exception { - repoInitializer.initializeRepository(); - contentRoot = repositoryObjectLoader.getContentRootObject(getContentRootPid()); - - PID unitPid = pidMinter.mintContentPid(); - unitObj = repoObjFactory.createAdminUnit(unitPid, - new AclModelBuilder("Admin unit") - .addUnitOwner("admin").model); - contentRoot.addMember(unitObj); - - PID collPid = pidMinter.mintContentPid(); - collObj = repoObjFactory.createCollectionObject(collPid, - new AclModelBuilder("Collection") - .addCanViewOriginals(PUBLIC_PRINC) - .addCanViewOriginals(AUTHENTICATED_PRINC).model); - - unitObj.addMember(collObj); - } - -} diff --git a/integration/src/test/resources/access-app.properties b/integration/src/test/resources/access-app.properties index 470bfdcbaa..0d6ebaa406 100644 --- a/integration/src/test/resources/access-app.properties +++ b/integration/src/test/resources/access-app.properties @@ -21,7 +21,7 @@ matomo.authToken= matomo.api.url= matomo.site.id=5 -loris.base.url=http://localhost:48080/loris/ +iiif.v2.base.url=http://localhost:48080/images/iiif/v2/ findingaids.base.url=https://finding-aids.lib.unc.edu/ findingaids.expireCacheSeconds=3600 diff --git a/integration/src/test/resources/loris-content-it-servlet.xml b/integration/src/test/resources/loris-content-it-servlet.xml deleted file mode 100644 index 1f9db3a517..0000000000 --- a/integration/src/test/resources/loris-content-it-servlet.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/integration/src/test/resources/loris-content-it.properties b/integration/src/test/resources/loris-content-it.properties deleted file mode 100644 index 16ea6523f2..0000000000 --- a/integration/src/test/resources/loris-content-it.properties +++ /dev/null @@ -1,9 +0,0 @@ -solr.protocol=http -solr.host=localhost -solr.port=:8983 -solr.context=solr - -access.group.admin=adminGrp - -loris.base.url=http://localhost:48085/loris -access.base.url=http://localhost:48085/ \ No newline at end of file diff --git a/web-access-app/src/main/webapp/WEB-INF/service-context.xml b/web-access-app/src/main/webapp/WEB-INF/service-context.xml index fdb3a2e605..522109e446 100644 --- a/web-access-app/src/main/webapp/WEB-INF/service-context.xml +++ b/web-access-app/src/main/webapp/WEB-INF/service-context.xml @@ -56,12 +56,6 @@ - - - - - - diff --git a/web-admin-app/src/test/resources/server.properties b/web-admin-app/src/test/resources/server.properties index 0f43e0913a..2f90d8b664 100644 --- a/web-admin-app/src/test/resources/server.properties +++ b/web-admin-app/src/test/resources/server.properties @@ -20,8 +20,6 @@ services.api.url=services/api/ fuseki.baseUri=http://localhost/fuseki -loris.base.url=http://localhost/loris - repository.protocol=https repository.host=localhost diff --git a/web-common/src/main/java/edu/unc/lib/boxc/web/common/controllers/LorisContentController.java b/web-common/src/main/java/edu/unc/lib/boxc/web/common/controllers/LorisContentController.java deleted file mode 100644 index 70ddd9311b..0000000000 --- a/web-common/src/main/java/edu/unc/lib/boxc/web/common/controllers/LorisContentController.java +++ /dev/null @@ -1,226 +0,0 @@ -package edu.unc.lib.boxc.web.common.controllers; - -import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; - -import java.io.IOException; -import java.util.List; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.ResponseBody; - -import com.fasterxml.jackson.core.JsonProcessingException; - -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.auth.fcrepo.services.GroupsThreadStore; -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.api.requests.SimpleIdRequest; -import edu.unc.lib.boxc.web.common.services.AccessCopiesService; -import edu.unc.lib.boxc.web.common.services.LorisContentService; - -/** - * Controller for requests related to accessing jp2's through loris. Applies cdr access control as a prerequisite to - * connecting with loris. - * - * @author bbpennel - */ -@Controller -public class LorisContentController extends AbstractSolrSearchController { - private static final Logger LOG = LoggerFactory.getLogger(LorisContentController.class); - - @Autowired - private LorisContentService lorisContentService; - - @Autowired - private AccessControlService accessControlService; - - @Autowired - private AccessCopiesService accessCopiesService; - - /** - * Determines if the user is allowed to access a specific datastream on the selected object. - * - * @param pid - * @param datastream - * @return - */ - private boolean hasAccess(PID pid, String datastream) { - // Defaults to jp2 surrogate - if (datastream == null) { - datastream = JP2_ACCESS_COPY.getId(); - } - - Permission permission = DatastreamPermissionUtil.getPermissionForDatastream(datastream); - - AgentPrincipals agent = AgentPrincipalsImpl.createFromThread(); - LOG.debug("Checking if user {} has access to {} belonging to object {}.", - agent.getUsername(), datastream, pid); - return accessControlService.hasAccess(pid, agent.getPrincipals(), permission); - } - - /** - * Handles requests for individual region tiles. - * @param id - * @param datastream - * @param region - * @param size - * @param rotation - * @param qualityFormat - * @param response - */ - @GetMapping("/jp2Proxy/{id}/{datastream}/{region}/{size}/{rotation}/{qualityFormat:.+}") - public void getRegion(@PathVariable("id") String id, - @PathVariable("datastream") String datastream, @PathVariable("region") String region, - @PathVariable("size") String size, @PathVariable("rotation") String rotation, - @PathVariable("qualityFormat") String qualityFormat, HttpServletResponse response) { - - PID pid = PIDs.get(id); - // Check if the user is allowed to view this object - if (this.hasAccess(pid, datastream)) { - try { - String[] qualityFormatArray = qualityFormat.split("\\."); - String quality = qualityFormatArray[0]; - String format = qualityFormatArray[1]; - response.addHeader("Access-Control-Allow-Origin", "*"); - lorisContentService.streamJP2( - id, region, size, rotation, quality, format, datastream, - response.getOutputStream(), response); - } catch (IOException e) { - LOG.error("Error retrieving streaming JP2 content for {}", id, e); - } - } else { - LOG.debug("Access was forbidden to {} for user {}", id, GroupsThreadStore.getUsername()); - response.setStatus(HttpStatus.FORBIDDEN.value()); - } - } - - /** - * Handles requests for jp2 metadata - * - * @param id - * @param datastream - * @param response - */ - @GetMapping("/jp2Proxy/{id}/{datastream}/info.json") - public void getMetadata(@PathVariable("id") String id, - @PathVariable("datastream") String datastream, HttpServletResponse response) { - PID pid = PIDs.get(id); - // Check if the user is allowed to view this object - if (this.hasAccess(pid, datastream)) { - try { - response.addHeader("Access-Control-Allow-Origin", "*"); - lorisContentService.getMetadata(id, response.getOutputStream(), response); - } catch (IOException e) { - LOG.error("Error retrieving JP2 metadata content for {}", id, e); - } - } else { - LOG.debug("Image access was forbidden to {} for user {}", id, GroupsThreadStore.getUsername()); - response.setStatus(HttpStatus.FORBIDDEN.value()); - } - } - - /** - * Handles requests for IIIF canvases - * @param id - * @param datastream - * @param response - * @return - */ - @GetMapping(value = "/jp2Proxy/{id}/{datastream}", produces = APPLICATION_JSON_VALUE) - @ResponseBody - public String getCanvas(@PathVariable("id") String id, @PathVariable("datastream") String datastream, - HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException { - PID pid = PIDs.get(id); - // Check if the user is allowed to view this object's manifest - if (this.hasAccess(pid, datastream)) { - SimpleIdRequest idRequest = new SimpleIdRequest(pid, GroupsThreadStore - .getAgentPrincipals().getPrincipals()); - ContentObjectRecord briefObj = queryLayer.getObjectById(idRequest); - response.addHeader("Access-Control-Allow-Origin", "*"); - return lorisContentService.getCanvas(request, briefObj); - } else { - LOG.debug("Manifest access was forbidden to {} for user {}", id, GroupsThreadStore.getUsername()); - response.setStatus(HttpStatus.FORBIDDEN.value()); - } - - return ""; - } - - /** - * Handles requests for IIIF sequences - * @param id - * @param datastream - * @param response - * @return - */ - @GetMapping(value = "/jp2Proxy/{id}/{datastream}/sequence/normal", produces = APPLICATION_JSON_VALUE) - @ResponseBody - public String getSequence(@PathVariable("id") String id, @PathVariable("datastream") String datastream, - HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException { - PID pid = PIDs.get(id); - // Check if the user is allowed to view this object's manifest - if (this.hasAccess(pid, datastream)) { - List briefObjs = getDatastreams(pid); - response.addHeader("Access-Control-Allow-Origin", "*"); - return lorisContentService.getSequence(request, briefObjs); - } else { - LOG.debug("Manifest access was forbidden to {} for user {}", id, GroupsThreadStore.getUsername()); - response.setStatus(HttpStatus.FORBIDDEN.value()); - } - - return ""; - } - - /** - * Handles requests for IIIF manifests - * @param id - * @param datastream - * @param response - * @return - */ - @GetMapping(value = "/jp2Proxy/{id}/{datastream}/manifest", produces = APPLICATION_JSON_VALUE) - @ResponseBody - public String getManifest(@PathVariable("id") String id, @PathVariable("datastream") String datastream, - HttpServletRequest request, HttpServletResponse response) { - PID pid = PIDs.get(id); - // Check if the user is allowed to view this object's manifest - if (this.hasAccess(pid, datastream)) { - try { - List briefObjs = getDatastreams(pid); - if (briefObjs.size() == 0) { - response.setStatus(HttpStatus.NOT_FOUND.value()); - } else { - response.addHeader("Access-Control-Allow-Origin", "*"); - return lorisContentService.getManifest(request, briefObjs); - } - } catch (IOException e) { - LOG.error("Error retrieving manifest content for {}", id, e); - } - } else { - LOG.debug("Manifest access was forbidden to {} for user {}", id, GroupsThreadStore.getUsername()); - response.setStatus(HttpStatus.FORBIDDEN.value()); - } - - return ""; - } - - private List getDatastreams(PID pid) { - AgentPrincipals agent = AgentPrincipalsImpl.createFromThread(); - return accessCopiesService.listViewableFiles(pid, agent.getPrincipals()); - } -} diff --git a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/LorisContentService.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/ImageServerV2Service.java similarity index 81% rename from web-common/src/main/java/edu/unc/lib/boxc/web/common/services/LorisContentService.java rename to web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/ImageServerV2Service.java index 00290b4cc0..60ebb52d23 100644 --- a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/LorisContentService.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/processing/ImageServerV2Service.java @@ -1,4 +1,4 @@ -package edu.unc.lib.boxc.web.common.services; +package edu.unc.lib.boxc.web.services.processing; import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.idToPath; @@ -9,6 +9,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import edu.unc.lib.boxc.web.services.utils.ImageServerUtil; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; @@ -43,17 +44,18 @@ import edu.unc.lib.boxc.web.common.utils.FileIOUtil; /** - * Generates request, connects to, and streams the output from loris. Sets pertinent headers. + * Generates request, connects to, and streams the output from a iiif v2 server. Sets pertinent headers. * @author bbpennel */ -public class LorisContentService { - private static final Logger LOG = LoggerFactory.getLogger(LorisContentService.class); +public class ImageServerV2Service { + private static final Logger LOG = LoggerFactory.getLogger(ImageServerV2Service.class); private CloseableHttpClient httpClient; private HttpClientConnectionManager httpClientConnectionManager; - private String lorisPath; + private String imageServerProxyBasePath; private String basePath; + private String accessAppPath; private ObjectMapper iiifMapper = new IiifObjectMapper(); public void setHttpClientConnectionManager(HttpClientConnectionManager manager) { @@ -77,9 +79,8 @@ public void getMetadata(String simplepid, OutputStream outStream, HttpServletRes public void getMetadata(String simplepid, OutputStream outStream, HttpServletResponse response, int retryServerError) { - StringBuilder path = new StringBuilder(getLorisPath()); - path.append(idToPath(simplepid, 4, 2)) - .append(simplepid).append(".jp2").append("/info.json"); + StringBuilder path = new StringBuilder(getImageServerProxyBasePath()); + path.append(ImageServerUtil.getImageServerEncodedId(simplepid)).append("/info.json"); int statusCode = -1; String statusLine = null; @@ -93,11 +94,9 @@ public void getMetadata(String simplepid, OutputStream outStream, response.setHeader("Content-Type", "application/json"); response.setHeader("content-disposition", "inline"); - ObjectMapper iiifMapper = new IiifObjectMapper(); - ImageService respData = iiifMapper.readValue(httpResp.getEntity().getContent(), ImageService.class); - respData.setIdentifier(new URI(URIUtil.join(basePath, "jp2Proxy", simplepid, "jp2"))); + respData.setIdentifier(new URI(URIUtil.join(basePath, "iiif", "v2", simplepid, "jp2"))); HttpEntity updatedRespData = EntityBuilder.create() .setText(iiifMapper.writeValueAsString(respData)) @@ -117,20 +116,24 @@ public void getMetadata(String simplepid, OutputStream outStream, } retryServerError--; } while (retryServerError >= 0 && (statusCode == 500 || statusCode == 404)); - LOG.error("Unexpected failure while getting Loris path {}: {}", statusLine, path); + LOG.error("Unexpected failure while getting image server path {}: {}", statusLine, path); } public void streamJP2(String simplepid, String region, String size, String rotatation, String quality, - String format, String datastream, OutputStream outStream, HttpServletResponse response ) { - this.streamJP2(simplepid, region, size, rotatation, quality, format, datastream, outStream, response, 1); + String format, OutputStream outStream, HttpServletResponse response ) { + this.streamJP2(simplepid, region, size, rotatation, quality, format, outStream, response, 1); } public void streamJP2(String simplepid, String region, String size, String rotation, String quality, - String format, String datastream, OutputStream outStream, HttpServletResponse response, + String format, OutputStream outStream, HttpServletResponse response, int retryServerError) { - StringBuilder path = new StringBuilder(getLorisPath()); - path.append(idToPath(simplepid, 4, 2)).append(simplepid).append(".jp2") + StringBuilder path = new StringBuilder(getImageServerProxyBasePath()); + // Remap "full" size to "max" since cantaloupe's v2 api doesn't seem to be able to handle full/full correctly + if ("full".equals(size)) { + size = ImageServerUtil.FULL_SIZE; + } + path.append(ImageServerUtil.getImageServerEncodedId(simplepid)) .append("/" + region).append("/" + size) .append("/" + rotation).append("/" + quality + "." + format); @@ -149,7 +152,7 @@ public void streamJP2(String simplepid, String region, String size, String rotat } else { if ((statusCode == 500 || statusCode == 404) && retryServerError > 0) { streamJP2(simplepid, region, size, rotation, quality, - format, datastream, outStream, response, retryServerError - 1); + format, outStream, response, retryServerError - 1); } else { LOG.error("Unexpected failure: {}", httpResp.getStatusLine()); LOG.error("Path was: {}", method.getURI()); @@ -164,9 +167,9 @@ public void streamJP2(String simplepid, String region, String size, String rotat } } - public String getManifest(HttpServletRequest request, List briefObjs) + public String getManifest(String id, String datastream, List briefObjs) throws JsonProcessingException { - String manifestBase = getRecordPath(request); + String manifestBase = getRecordPath(id, datastream); ContentObjectRecord rootObj = briefObjs.get(0); String title = getTitle(rootObj); @@ -188,7 +191,7 @@ public String getManifest(HttpServletRequest request, List setMetadataField(manifest, "Subjects", subjects); setMetadataField(manifest, "Languages", language); manifest.addMetadata("", "View full record"); + URIUtil.join(accessAppPath, "record", rootObj.getId()) + "\">View full record"); String attribution = "University of North Carolina Libraries, Digital Collections Repository"; String collection = rootObj.getParentCollectionName(); if (collection != null) { @@ -201,15 +204,15 @@ public String getManifest(HttpServletRequest request, List return iiifMapper.writeValueAsString(manifest.addSequence(seq)); } - public String getSequence(HttpServletRequest request, List briefObjs) + public String getSequence(String id, String datastream, List briefObjs) throws JsonProcessingException { - String path = getRecordPath(request); + String path = getRecordPath(id, datastream); return iiifMapper.writeValueAsString(createSequence(path, briefObjs)); } - public String getCanvas(HttpServletRequest request, ContentObjectRecord briefObj) + public String getCanvas(String id, String datastream, ContentObjectRecord briefObj) throws JsonProcessingException { - String path = getRecordPath(request); + String path = getRecordPath(id, datastream); return iiifMapper.writeValueAsString(createCanvas(path, briefObj)); } @@ -256,7 +259,7 @@ private Canvas createCanvas(String path, ContentObjectRecord briefObj) { return canvas; } - String canvasPath = URIUtil.join(basePath, "jp2Proxy", uuid, "jp2"); + String canvasPath = URIUtil.join(basePath, "iiif", "v2", uuid, "jp2"); Datastream fileDs = briefObj.getDatastreamObject(DatastreamType.ORIGINAL_FILE.getId()); String extent = fileDs.getExtent(); @@ -274,11 +277,8 @@ private Canvas createCanvas(String path, ContentObjectRecord briefObj) { return canvas; } - private String getRecordPath(HttpServletRequest request) { - String[] url = request.getRequestURL().toString().split("\\/"); - String uuid = url[4]; - String datastream = url[5]; - return URIUtil.join(basePath, "jp2Proxy", uuid, datastream); + private String getRecordPath(String uuid, String datastream) { + return URIUtil.join(basePath, "iiif", "v2", uuid, datastream); } private String jp2Pid(ContentObjectRecord briefObj) { @@ -305,12 +305,12 @@ private String getTitle(ContentObjectRecord briefObj) { return (title != null) ? title : ""; } - public void setLorisPath(String fullPath) { - this.lorisPath = fullPath; + public void setImageServerProxyBasePath(String fullPath) { + this.imageServerProxyBasePath = fullPath; } - public String getLorisPath() { - return lorisPath; + public String getImageServerProxyBasePath() { + return imageServerProxyBasePath; } public void setBasePath(String basePath) { @@ -320,4 +320,8 @@ public void setBasePath(String basePath) { public String getBasePath() { return basePath; } + + public void setAccessAppPath(String accessAppPath) { + this.accessAppPath = accessAppPath; + } } diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/ImageServerV2Controller.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/ImageServerV2Controller.java new file mode 100644 index 0000000000..e8b394c2bb --- /dev/null +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/ImageServerV2Controller.java @@ -0,0 +1,203 @@ +package edu.unc.lib.boxc.web.services.rest; + +import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import java.io.IOException; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import edu.unc.lib.boxc.web.common.controllers.AbstractSolrSearchController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.fasterxml.jackson.core.JsonProcessingException; + +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.auth.fcrepo.services.GroupsThreadStore; +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.api.requests.SimpleIdRequest; +import edu.unc.lib.boxc.web.common.services.AccessCopiesService; +import edu.unc.lib.boxc.web.services.processing.ImageServerV2Service; + +/** + * Controller for requests related to accessing jp2's through a v2 iiif server. Applies boxc access control + * + * @author bbpennel + */ +@Controller +public class ImageServerV2Controller extends AbstractSolrSearchController { + private static final Logger LOG = LoggerFactory.getLogger(ImageServerV2Controller.class); + + @Autowired + private ImageServerV2Service imageServerV2Service; + + @Autowired + private AccessControlService accessControlService; + + @Autowired + private AccessCopiesService accessCopiesService; + + private void assertHasAccess(PID pid) { + AgentPrincipals agent = AgentPrincipalsImpl.createFromThread(); + accessControlService.assertHasAccess("Insufficient permissions for " + pid.getId(), + pid, agent.getPrincipals(), Permission.viewAccessCopies); + } + + /** + * Handles requests for individual region tiles. + * @param id + * @param datastream + * @param region + * @param size + * @param rotation + * @param qualityFormat + * @param response + */ + @GetMapping("/iiif/v2/{id}/{datastream}/{region}/{size}/{rotation}/{qualityFormat:.+}") + public void getRegion(@PathVariable("id") String id, + @PathVariable("datastream") String datastream, @PathVariable("region") String region, + @PathVariable("size") String size, @PathVariable("rotation") String rotation, + @PathVariable("qualityFormat") String qualityFormat, HttpServletResponse response) { + + PID pid = PIDs.get(id); + // Check if the user is allowed to view this object + assertHasAccess(pid); + try { + String[] qualityFormatArray = qualityFormat.split("\\."); + String quality = qualityFormatArray[0]; + String format = qualityFormatArray[1]; + response.addHeader("Access-Control-Allow-Origin", "*"); + imageServerV2Service.streamJP2( + id, region, size, rotation, quality, format, + response.getOutputStream(), response); + } catch (IOException e) { + LOG.error("Error retrieving streaming JP2 content for {}", id, e); + } + } + + /** + * Handles requests for jp2 metadata + * + * @param id + * @param datastream + * @param response + */ + @GetMapping("/iiif/v2/{id}/{datastream}/info.json") + public void getMetadata(@PathVariable("id") String id, + @PathVariable("datastream") String datastream, HttpServletResponse response) { + PID pid = PIDs.get(id); + // Check if the user is allowed to view this object + assertHasAccess(pid); + try { + addAllowOriginHeader(response); + imageServerV2Service.getMetadata(id, response.getOutputStream(), response); + } catch (IOException e) { + LOG.error("Error retrieving JP2 metadata content for {}", id, e); + } + } + + private void addAllowOriginHeader(HttpServletResponse response) { + response.addHeader("Access-Control-Allow-Origin", "*"); + } + + /** + * Handles requests for IIIF canvases + * @param id + * @param datastream + * @param response + * @return + */ + @GetMapping(value = "/iiif/v2/{id}/{datastream}", produces = APPLICATION_JSON_VALUE) + @ResponseBody + public String getCanvas(@PathVariable("id") String id, @PathVariable("datastream") String datastream, + HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException { + PID pid = PIDs.get(id); + // Check if the user is allowed to view this object's manifest + assertHasAccess(pid); + SimpleIdRequest idRequest = new SimpleIdRequest(pid, GroupsThreadStore + .getAgentPrincipals().getPrincipals()); + ContentObjectRecord briefObj = queryLayer.getObjectById(idRequest); + addAllowOriginHeader(response); + return imageServerV2Service.getCanvas(id, datastream, briefObj); + } + + /** + * Handles requests for IIIF sequences + * @param id + * @param datastream + * @param response + * @return + */ + @GetMapping(value = "/iiif/v2/{id}/{datastream}/sequence/normal", produces = APPLICATION_JSON_VALUE) + @ResponseBody + public String getSequence(@PathVariable("id") String id, @PathVariable("datastream") String datastream, + HttpServletRequest request, HttpServletResponse response) throws JsonProcessingException { + PID pid = PIDs.get(id); + // Check if the user is allowed to view this object's manifest + assertHasAccess(pid); + List briefObjs = getDatastreams(pid); + addAllowOriginHeader(response); + return imageServerV2Service.getSequence(id, datastream, briefObjs); + } + + /** + * Handles requests for IIIF manifests + * @param id + * @param datastream + * @param response + * @return + */ + @GetMapping(value = "/iiif/v2/{id}/{datastream}/manifest" , produces = APPLICATION_JSON_VALUE) + @ResponseBody + public String getManifest(@PathVariable("id") String id, @PathVariable("datastream") String datastream, + HttpServletRequest request, HttpServletResponse response) { + PID pid = PIDs.get(id); + // Check if the user is allowed to view this object's manifest + assertHasAccess(pid); + try { + List briefObjs = getDatastreams(pid); + if (briefObjs.size() == 0) { + response.setStatus(HttpStatus.NOT_FOUND.value()); + } else { + addAllowOriginHeader(response); + return imageServerV2Service.getManifest(id, datastream, briefObjs); + } + } catch (IOException e) { + LOG.error("Error retrieving manifest content for {}", id, e); + } + + return ""; + } + + private List getDatastreams(PID pid) { + AgentPrincipals agent = AgentPrincipalsImpl.createFromThread(); + return accessCopiesService.listViewableFiles(pid, agent.getPrincipals()); + } + + public void setImageServerV2Service(ImageServerV2Service imageServerV2Service) { + this.imageServerV2Service = imageServerV2Service; + } + + public void setAccessControlService(AccessControlService accessControlService) { + this.accessControlService = accessControlService; + } + + public void setAccessCopiesService(AccessCopiesService accessCopiesService) { + this.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 fe76f8785a..ffc66b9c8f 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 @@ -420,7 +420,7 @@ - + @@ -433,6 +433,13 @@ + + + + + + + @@ -442,7 +449,7 @@ - + diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java index 075a05b665..2d54041815 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java @@ -2,7 +2,7 @@ package edu.unc.lib.boxc.web.services.rest; import static edu.unc.lib.boxc.auth.api.Permission.viewReducedResImages; -import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.idToPath; + import com.github.tomakehurst.wiremock.client.WireMock; import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; import edu.unc.lib.boxc.auth.api.services.AccessControlService; @@ -14,7 +14,6 @@ import edu.unc.lib.boxc.search.solr.models.ContentObjectSolrRecord; import edu.unc.lib.boxc.web.common.services.SolrQueryLayerService; import edu.unc.lib.boxc.web.services.processing.DownloadImageService; -import edu.unc.lib.boxc.web.services.processing.ImageServerProxyService; import edu.unc.lib.boxc.web.services.rest.modify.AbstractAPIIT; import edu.unc.lib.boxc.web.services.utils.ImageServerUtil; import org.apache.commons.io.FileUtils; diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/ImageServerV2ControllerTest.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/ImageServerV2ControllerTest.java new file mode 100644 index 0000000000..8761c44c41 --- /dev/null +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/ImageServerV2ControllerTest.java @@ -0,0 +1,182 @@ +package edu.unc.lib.boxc.web.services.rest; + +import de.digitalcollections.iiif.model.jackson.IiifObjectMapper; +import de.digitalcollections.iiif.model.openannotation.Annotation; +import de.digitalcollections.iiif.model.sharedcanvas.Canvas; +import de.digitalcollections.iiif.model.sharedcanvas.Manifest; +import de.digitalcollections.iiif.model.sharedcanvas.Sequence; +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.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 edu.unc.lib.boxc.web.services.processing.ImageServerV2Service; +import edu.unc.lib.boxc.web.services.rest.exceptions.RestResponseEntityExceptionHandler; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +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.mock.web.MockHttpServletResponse; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +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 ImageServerV2ControllerTest { + private static final String IIIF_BASE = "http://example.com/iiif/v2/"; + 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 String FILE_ID = "b150dca9-c4cf-4651-aeef-3ce9e279178f"; + private static final PID OBJECT_PID = PIDs.get(OBJECT_ID); + private static final PID FILE_PID = PIDs.get(FILE_ID); + private final static String USERNAME = "test_user"; + private final static AccessGroupSet GROUPS = new AccessGroupSetImpl("adminGroup"); + + @InjectMocks + private ImageServerV2Controller imageController; + + private HttpClientConnectionManager httpClientConnectionManager; + + @Mock + private AccessControlService accessControlService; + + @Mock + private AccessCopiesService accessCopiesService; + + private ImageServerV2Service imageService; + + private MockMvc mockMvc; + + private AutoCloseable closeable; + + @BeforeEach + public void setup() { + closeable = openMocks(this); + imageService = new ImageServerV2Service(); + imageService.setImageServerProxyBasePath(IIIF_BASE); + imageService.setBasePath(SERVICES_BASE); + imageService.setAccessAppPath(ACCESS_BASE); + httpClientConnectionManager = new PoolingHttpClientConnectionManager(); + imageService.setHttpClientConnectionManager(httpClientConnectionManager); + imageController.setImageServerV2Service(imageService); + mockMvc = MockMvcBuilders.standaloneSetup(imageController) + .setControllerAdvice(new RestResponseEntityExceptionHandler()) + .build(); + GroupsThreadStore.storeUsername(USERNAME); + GroupsThreadStore.storeGroups(GROUPS); + when(accessControlService.hasAccess(any(), any(), any())).thenReturn(true); + } + + @AfterEach + void closeService() throws Exception { + httpClientConnectionManager.shutdown(); + 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"); + var fileObj = new ContentObjectSolrRecord(); + fileObj.setId(FILE_ID); + fileObj.setResourceType(ResourceType.File.name()); + fileObj.setTitle("file"); + var originalDs = new DatastreamImpl("original_file|image/jpeg|image.jpg|jpg|0|||375x250"); + var jp2Ds = new DatastreamImpl("jp2|image/jp2|image.jp2|jp2|0|||"); + fileObj.setDatastream(Arrays.asList(originalDs.toString(), jp2Ds.toString())); + var viewableRecords = new ArrayList(Arrays.asList(workObj, fileObj)); + when(accessCopiesService.listViewableFiles(eq(OBJECT_PID), any())).thenReturn(viewableRecords); + + var result = mockMvc.perform(get("/iiif/v2/" + OBJECT_ID + "/jp2/manifest") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + Manifest respManifest = parseManifestResponse(result); + assertHasImageManifest(respManifest, FILE_PID, "Test Work", "file"); + } + + @Test + public void testGetManifestNoAccess() throws Exception { + doThrow(new AccessRestrictionException()).when(accessControlService) + .assertHasAccess(anyString(), any(), any(), eq(Permission.viewAccessCopies)); + + mockMvc.perform(get("/iiif/v2/" + OBJECT_ID + "/jp2/manifest") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + public void testGetManifestNoImages() throws Exception { + when(accessCopiesService.listViewableFiles(eq(OBJECT_PID), any())).thenReturn(Collections.emptyList()); + + mockMvc.perform(get("/iiif/v2/" + OBJECT_ID + "/jp2/manifest") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()) + .andReturn(); + } + + private void assertHasImageManifest(Manifest manifest, PID filePid, String label, String fileLabel) throws Exception { + assertEquals(label, manifest.getLabelString()); + Sequence sequence = manifest.getSequences().get(0); + List canvases = sequence.getCanvases(); + assertEquals(1, canvases.size()); + assertManifestContainsImage(manifest, 0, filePid, fileLabel); + } + + private void assertManifestContainsImage(Manifest manifest, int index, PID filePid, String label) { + Sequence sequence = manifest.getSequences().get(0); + List canvases = sequence.getCanvases(); + Canvas canvas = canvases.get(index); + assertEquals(label, canvas.getLabelString()); + assertEquals(375, canvas.getHeight().intValue()); + assertEquals(250, canvas.getWidth().intValue()); + List images = canvas.getImages(); + assertEquals(2, images.size()); + Annotation jp2Image = images.get(0); + assertEquals("http://example.com/services/iiif/v2/" + filePid.getId() + "/jp2", + jp2Image.getResource().getServices().get(0).getIdentifier().toString()); + Annotation thumbImage = images.get(1); + assertEquals("http://example.com/services/services/api/thumb/" + filePid.getId() + "/large", + thumbImage.getResource().getIdentifier().toString()); + } + + private Manifest parseManifestResponse(MvcResult result) throws Exception { + MockHttpServletResponse response = result.getResponse(); + Manifest manifest = new IiifObjectMapper().readValue(response.getContentAsString(), Manifest.class); + return manifest; + } +}