Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BXC-4330 - IIIFv3 manifests #1620

Merged
merged 7 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions web-services-app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.freelibrary</groupId>
<artifactId>jiiify-presentation-v3</artifactId>
<!-- newer versions are built with java 17 -->
<version>0.12.3</version>
</dependency>
<!-- Servlets -->
<dependency>
<groupId>jakarta.servlet</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Metadata> constructMetadataSection(ContentObjectRecord rootObj) {
var metadataList = new ArrayList<Metadata>();
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("", "<a href=\"" +
sharonluong marked this conversation as resolved.
Show resolved Hide resolved
URIUtil.join(baseAccessPath, "record", rootObj.getId()) + "\">View full record</a>"));
return metadataList;
}

private void addMultiValuedMetadataField(List<Metadata> metadataList, String fieldName, List<String> 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<ContentObjectRecord> contentObjs) {
var canvases = new ArrayList<Canvas>();
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) {
sharonluong marked this conversation as resolved.
Show resolved Hide resolved
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<PaintingAnnotation>(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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Object> 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<Object> 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;
}
}
8 changes: 8 additions & 0 deletions web-services-app/src/main/webapp/WEB-INF/service-context.xml
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,14 @@
<bean id="thumbnailRequestSender" class="edu.unc.lib.boxc.operations.jms.thumbnails.ThumbnailRequestSender">
<property name="jmsTemplate" ref="thumbnailRequestJmsTemplate"/>
</bean>

<bean id="iiifV3ManifestService" class="edu.unc.lib.boxc.web.services.processing.IiifV3ManifestService">
<property name="baseIiifv3Path" value="${services.api.url}iiif/v3/"/>
<property name="baseAccessPath" value="${repository.protocol}://${repository.host}/"/>
<property name="baseServicesApiPath" value="${services.api.url}"/>
<property name="accessControlService" ref="aclService"/>
<property name="accessCopiesService" ref="accessCopiesService"/>
</bean>

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="edu.unc.lib.boxc.web.common.utils.SerializationUtil.injectSettings"/>
Expand Down
Loading
Loading