Added iiif service and utility classes.

This commit is contained in:
Michael Spalti
2021-03-25 15:52:21 -07:00
parent 1a353d4bd5
commit 1a6d3d4ee3
9 changed files with 1362 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service;
import java.util.UUID;
import org.dspace.app.rest.iiif.model.generator.CanvasGenerator;
import org.dspace.app.rest.iiif.model.generator.ImageContentGenerator;
import org.dspace.app.rest.iiif.model.generator.ImageServiceGenerator;
import org.dspace.app.rest.iiif.model.generator.ProfileGenerator;
import org.dspace.app.rest.iiif.service.util.IIIFUtils;
import org.dspace.app.rest.iiif.service.util.ImageProfileUtil;
import org.dspace.app.rest.iiif.service.util.ThumbProfileUtil;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Base class for IIIF responses.
*/
public abstract class AbstractResourceService {
/**
* These values are defined in dspace configuration.
*/
protected String IIIF_ENDPOINT;
protected String IMAGE_SERVICE;
protected String SEARCH_URL;
protected String CLIENT_URL;
protected String BITSTREAM_PATH_PREFIX;
/**
* Possible values: "paged" or "individuals". Typically paged is preferred
* for documents. However, it can be overridden in configuration if necessary
* for the viewer client.
*/
protected static String DOCUMENT_VIEWING_HINT;
// TODO: should these bundle settings be added to dspace configuration or hard-coded here?
// The DSpace bundle used for IIIF entity types.
protected static final String IIIF_BUNDLE = "IIIF";
// The DSpace bundle for other content related to item.
protected static final String OTHER_CONTENT_BUNDLE = "OtherContent";
// Paths for IIIF Image API requests.
protected static final String THUMBNAIL_PATH = "/full/,90/0/default.jpg";
protected static final String IMAGE_PATH = "/full/full/0/default.jpg";
@Autowired
IIIFUtils utils;
@Autowired
ThumbProfileUtil thumbUtil;
@Autowired
ImageProfileUtil imageUtil;
@Autowired
ImageContentGenerator imageContent;
@Autowired
ImageServiceGenerator imageService;
/**
* Set constants using DSpace configuration definitions.
* @param configurationService the DSpace configuration service
*/
protected void setConfiguration(ConfigurationService configurationService) {
IIIF_ENDPOINT = configurationService.getProperty("iiif.url");
IMAGE_SERVICE = configurationService.getProperty("iiif.image.server");
SEARCH_URL = configurationService.getProperty("iiif.solr.search.url");
BITSTREAM_PATH_PREFIX = configurationService.getProperty("iiif.bitstream.url");
DOCUMENT_VIEWING_HINT = configurationService.getProperty("iiif.document.viewing.hint");
CLIENT_URL = configurationService.getProperty("dspace.ui.url");
}
/**
* Creates the manifest id from the provided uuid.
* @param uuid the item id
* @return the manifest identifier (url)
*/
protected String getManifestId(UUID uuid) {
return IIIF_ENDPOINT + uuid + "/manifest";
}
/**
* Association of images with their respective canvases is done via annotations.
* Only the annotations that associate images or parts of images are included in
* the canvas in the images property. If a IIIF Image API service is available for
* the image, then a link to the services base URI should be included.
*
* This method adds an image annotations to a canvas for both thumbnail and full size
* images. The annotation references the IIIF image service.
*
* @param canvas the Canvas object.
* @param mimeType the image mime type
* @param bitstreamID the bitstream uuid
*/
protected void addImage(CanvasGenerator canvas, String mimeType, UUID bitstreamID) throws
RuntimeException {
canvas.addThumbnail(getThumbnailAnnotation(bitstreamID, mimeType));
// Add image content resource to canvas facade.
canvas.addImage(getImageContent(bitstreamID, mimeType, imageUtil.getImageProfile(), IMAGE_PATH).getResource());
}
/**
* A small image that depicts or pictorially represents the resource that
* the property is attached to, such as the title page, a significant image
* or rendering of a canvas with multiple content resources associated with it.
* It is recommended that a IIIF Image API service be available for this image for
* manipulations such as resizing.
*
* This method returns a thumbnail annotation that includes the IIIF image service.
*
* @param uuid the bitstream id
* @return thumbnail Annotation
*/
protected ImageContentGenerator getThumbnailAnnotation(UUID uuid, String mimetype) throws
RuntimeException {
return getImageContent(uuid, mimetype, thumbUtil.getThumbnailProfile(), THUMBNAIL_PATH);
}
/**
* Association of images with their respective canvases is done via annotations. The Open Annotation model
* allows any resource to be associated with any other resource, or parts thereof, and it is reused for
* both commentary and painting resources on the canvas.
* @param uuid bitstream uuid
* @param mimetype bitstream mimetype
* @param profile the service profile
* @param path the path component of the identifier
* @return
*/
private ImageContentGenerator getImageContent(UUID uuid, String mimetype, ProfileGenerator profile, String path) {
imageContent.setFormat(mimetype);
imageContent.setIdentifier(IMAGE_SERVICE + uuid + path);
imageContent.addService(getImageService(profile, uuid.toString()));
return imageContent;
}
/**
* A link to a service that makes more functionality available for the resource,
* such as from an image to the base URI of an associated IIIF Image API service.
*
* @param profile service profile
* @param uuid id of the image bitstream
* @return object representing the Image Service
*/
private ImageServiceGenerator getImageService(ProfileGenerator profile, String uuid) {
imageService.setIdentifier(IMAGE_SERVICE + uuid);
imageService.setProfile(profile);
return imageService;
}
}

View File

@@ -0,0 +1,121 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import org.dspace.app.rest.iiif.model.generator.AnnotationGenerator;
import org.dspace.app.rest.iiif.model.generator.AnnotationListGenerator;
import org.dspace.app.rest.iiif.model.generator.ExternalLinksGenerator;
import org.dspace.app.rest.iiif.model.generator.PropertyValueGenerator;
import org.dspace.app.rest.iiif.service.util.IIIFUtils;
import org.dspace.content.Bitstream;
import org.dspace.content.BitstreamFormat;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
import org.dspace.content.service.BitstreamFormatService;
import org.dspace.content.service.BitstreamService;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AnnotationListService extends AbstractResourceService {
@Autowired
IIIFUtils utils;
@Autowired
ItemService itemService;
@Autowired
BitstreamService bitstreamService;
@Autowired
BitstreamFormatService bitstreamFormatService;
@Autowired
PropertyValueGenerator propertyValue;
@Autowired
AnnotationListGenerator annotationList;
@Autowired
AnnotationGenerator annotation;
@Autowired
ExternalLinksGenerator otherContent;
public AnnotationListService(ConfigurationService configurationService) {
setConfiguration(configurationService);
}
/**
* Returns an AnnotationList for bitstreams in the OtherContent bundle.
* These resources are not appended directly to the manifest but can be accessed
* via the seeAlso link.
*
* The semantics of this linking property may be extended to full text files, but
* machine readable formats like ALTO, METS, and schema.org descriptions are preferred.
*
* @param context DSpace context
* @param id bitstream uuid
* @return AnnotationList as JSON
*/
public String getSeeAlsoAnnotations(Context context, UUID id)
throws RuntimeException {
/**
* We need the DSpace item to proceed.
*/
Item item;
try {
item = itemService.find(context, id);
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
/**
* AnnotationList requires an identifier.
*/
annotationList.setIdentifier(IIIF_ENDPOINT + id + "/manifest/seeAlso");
/**
* Get the "OtherContent" bundle for the item. Then add
* Annotations for each bitstream found in the bundle.
*/
List<Bundle> bundles = utils.getBundle(item, OTHER_CONTENT_BUNDLE);
if (bundles.size() > 0) {
for (Bundle bundle : bundles) {
List<Bitstream> bitstreams = bundle.getBitstreams();
for (Bitstream bitstream : bitstreams) {
BitstreamFormat format;
String mimetype;
try {
format = bitstream.getFormat(context);
mimetype = format.getMIMEType();
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
}
annotation.setIdentifier(IIIF_ENDPOINT + bitstream.getID() + "/annot");
annotation.setMotivation(AnnotationGenerator.LINKING);
otherContent.setIdentifier(BITSTREAM_PATH_PREFIX
+ "/"
+ bitstream.getID()
+ "/content");
otherContent.setFormat(mimetype);
otherContent.setLabel(bitstream.getName());
annotation.setResource(otherContent);
annotationList.addResource(annotation);
}
}
}
return utils.asJson(annotationList.getResource());
}
}

View File

@@ -0,0 +1,59 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service;
import java.util.ArrayList;
import java.util.UUID;
import org.dspace.app.rest.iiif.model.generator.CanvasGenerator;
import org.dspace.app.rest.iiif.model.info.Info;
import org.dspace.app.rest.iiif.service.util.IIIFUtils;
import org.dspace.content.Bitstream;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.stereotype.Component;
/**
* Canvases may be dereferenced separately from the manifest via their URIs.
*/
@Component
public class CanvasLookupService extends AbstractResourceService {
@Autowired
IIIFUtils utils;
@Autowired
CanvasService canvasService;
public CanvasLookupService(ConfigurationService configurationService) {
setConfiguration(configurationService);
}
public String generateCanvas(Context context, Item item, String canvasId) {
int canvasPosition = utils.getCanvasId(canvasId);
Bitstream bitstream = utils.getBitstreamForCanvas(item, IIIF_BUNDLE, canvasPosition);
if (bitstream == null) {
throw new ResourceNotFoundException();
}
Info info =
utils.validateInfoForSingleCanvas(utils.getInfo(context, item, IIIF_BUNDLE), canvasPosition);
ArrayList<Bitstream> bitstreams = new ArrayList<>();
bitstreams.add(bitstream);
UUID bitstreamID = bitstream.getID();
String mimeType = utils.getBitstreamMimeType(bitstream, context);
CanvasGenerator canvas = canvasService.getCanvas(item.getID().toString(), info, canvasPosition);
if (mimeType.contains("image/")) {
addImage(canvas, mimeType, bitstreamID);
}
return utils.asJson(canvas.getResource());
}
}

View File

@@ -0,0 +1,93 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service;
import org.apache.log4j.Logger;
import org.dspace.app.rest.iiif.model.generator.CanvasGenerator;
import org.dspace.app.rest.iiif.model.info.Info;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class CanvasService extends AbstractResourceService {
private static final Logger log = Logger.getLogger(CanvasService.class);
// Default canvas dimensions.
protected static final Integer DEFAULT_CANVAS_WIDTH = 1200;
protected static final Integer DEFAULT_CANVAS_HEIGHT = 1600;
@Autowired
CanvasGenerator canvas;
/**
* Constructor.
* @param configurationService the DSpace configuration service.
*/
public CanvasService(ConfigurationService configurationService) {
setConfiguration(configurationService);
}
/**
* Creates a single Canvas object. If canvas parameters are provided by the
* Info object they are used. If canvas parameters are unavailable, default values
* are used instead.
*
* @param id manifest id
* @param info parameters for this canvas
* @param count the canvas position in the sequence.
* @return canvas object
*/
protected CanvasGenerator getCanvas(String id, Info info, int count) {
// Defaults settings.
int canvasWidth = DEFAULT_CANVAS_WIDTH;
int canvasHeight = DEFAULT_CANVAS_HEIGHT;
int pagePosition = count + 1;
String label = "Page " + pagePosition;
// Override with settings from info.json, if available.
if (info != null && info.getGlobalDefaults() != null && info.getCanvases() != null) {
// Use global settings if activated.
if (info.getGlobalDefaults().isActivated()) {
// Create unique label by appending position to the default label.
label = info.getGlobalDefaults().getLabel() + " " + pagePosition;
canvasWidth = info.getGlobalDefaults().getWidth();
canvasHeight = info.getGlobalDefaults().getHeight();
} else if (info.getCanvases().get(count) != null) {
if (info.getCanvases().get(count).getLabel().length() > 0) {
// Individually defined canvas labels assumed unique, and are not incremented.
label = info.getCanvases().get(count).getLabel();
}
canvasWidth = info.getCanvases().get(count).getWidth();
canvasHeight = info.getCanvases().get(count).getHeight();
}
} else {
log.info("Correctly formatted info.json was not found for item. Using application defaults.");
}
canvas.setIdentifier(IIIF_ENDPOINT + id + "/canvas/c" + count);
canvas.setLabel(label);
canvas.setHeight(canvasHeight);
canvas.setWidth(canvasWidth);
return canvas;
}
/**
* Ranges expect the Canvas object to have only an identifier. This method assures that the
* injected canvas facade is empty before setting the identifier.
* @param identifier the DSpace item identifier
* @param startCanvas the position of the canvas in list
* @return
*/
protected CanvasGenerator getRangeCanvasReference(String identifier, String startCanvas) {
canvas.setIdentifier(IIIF_ENDPOINT + identifier + startCanvas);
return canvas;
}
}

View File

@@ -0,0 +1,338 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import de.digitalcollections.iiif.model.sharedcanvas.AnnotationList;
import org.apache.log4j.Logger;
import org.dspace.app.rest.iiif.model.generator.CanvasGenerator;
import org.dspace.app.rest.iiif.model.generator.CanvasItemsGenerator;
import org.dspace.app.rest.iiif.model.generator.ContentSearchGenerator;
import org.dspace.app.rest.iiif.model.generator.ExternalLinksGenerator;
import org.dspace.app.rest.iiif.model.generator.ManifestGenerator;
import org.dspace.app.rest.iiif.model.generator.RangeGenerator;
import org.dspace.app.rest.iiif.model.info.Info;
import org.dspace.app.rest.iiif.model.info.RangeModel;
import org.dspace.app.rest.iiif.service.util.IIIFUtils;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Generates IIIF Manifest JSON response for a DSpace Item.
*/
@Component
public class ManifestService extends AbstractResourceService {
private static final Logger log = Logger.getLogger(ManifestService.class);
private static final String PDF_DOWNLOAD_LABEL = "Download as PDF";
private static final String RELATED_ITEM_LABEL = "DSpace item view";
private static final String SEE_ALSO_LABEL = "More descriptions of this resource";
@Autowired
protected ItemService itemService;
@Autowired
CanvasService canvasService;
@Autowired
ExternalLinksGenerator otherContentGenerator;
@Autowired
ManifestGenerator manifestGenerator;
@Autowired
CanvasItemsGenerator sequenceGenerator;
@Autowired
RangeGenerator rangeGenerator;
@Autowired
ContentSearchGenerator contentSearchGenerator;
@Autowired
IIIFUtils utils;
/**
* Constructor.
* @param configurationService the DSpace configuration service.
*/
public ManifestService(ConfigurationService configurationService) {
setConfiguration(configurationService);
}
/**
* Returns serialized Manifest response for a DSpace item.
*
* @param item the DSpace Item
* @param context the DSpace context
* @return Manifest as JSON
*/
public String getManifest(Item item, Context context) {
initializeManifestGenerator(item, context);
return utils.asJson(manifestGenerator.getResource());
}
/**
* Initializes the Manifest for a DSpace item.
*
* @param item DSpace Item
* @param context DSpace context
* @return manifest object
*/
private void initializeManifestGenerator(Item item, Context context) {
List<Bundle> bundles = utils.getIiifBundle(item, IIIF_BUNDLE);
List<Bitstream> bitstreams = utils.getBitstreams(bundles);
Info info = utils.validateInfoForManifest(utils.getInfo(context, item, IIIF_BUNDLE), bitstreams);
manifestGenerator.setIdentifier(getManifestId(item.getID()));
manifestGenerator.setLabel(item.getName());
addRelated(item);
addSearchService(item);
addMetadata(item.getMetadata());
addViewingHint(bitstreams.size());
addThumbnail(bitstreams, context);
addSequence(item, bitstreams, context, info);
addRanges(info, item.getID().toString());
addSeeAlso(item);
}
/**
* Returns a single sequence with canvases and item rendering (optional).
* @param item DSpace Item
* @param bitstreams list of bitstreams
* @param context the DSpace context
* @return a sequence of canvases
*/
private void addSequence(Item item, List<Bitstream> bitstreams, Context context, Info info) {
sequenceGenerator.setIdentifier(IIIF_ENDPOINT + item.getID() + "/sequence/s0");
if (bitstreams.size() > 0) {
addCanvas(sequenceGenerator, context, item, bitstreams, info);
}
addRendering(sequenceGenerator, item, context);
manifestGenerator.addSequence(sequenceGenerator);
}
/**
* Adds DSpace Item metadata to the manifest.
*
* @param metadata list of DSpace metadata values
*/
private void addMetadata(List<MetadataValue> metadata) {
for (MetadataValue meta : metadata) {
String field = utils.getMetadataFieldName(meta);
if (field.contentEquals("rights.uri")) {
manifestGenerator.addMetadata(field, meta.getValue());
manifestGenerator.addLicense(meta.getValue());
} else if (field.contentEquals("description")) {
// Add manifest description field.
manifestGenerator.addDescription(field, meta.getValue());
} else {
// Exclude DSpace description.provenance field.
if (!field.contentEquals("description.provenance")) {
// Everything else, add to manifest metadata fields.
manifestGenerator.addMetadata(field, meta.getValue());
}
}
}
}
/**
* A link to an external resource intended to be displayed directly to the user,
* and is related to the resource that has the related property. Examples might
* include a video or academic paper about the resource, a website, an HTML
* description, and so forth.
*
* This method adds a link to the Item represented in the DSpace Angular UI.
*
* @param item the DSpace Item
*/
private void addRelated(Item item) {
String url = CLIENT_URL + "/items/" + item.getID();
otherContentGenerator.setIdentifier(url);
otherContentGenerator.setFormat("text/html");
otherContentGenerator.setLabel(RELATED_ITEM_LABEL);
manifestGenerator.addRelated(otherContentGenerator);
}
/**
* This method adds a canvas to the sequence for each item in the list of DSpace bitstreams.
* To be added bitstreams must be on image mime type.
*
* @param sequence the sequence object
* @param context the DSpace context
* @param item the DSpace Item
* @param bitstreams list of DSpace bitstreams
*/
private void addCanvas(CanvasItemsGenerator sequence, Context context, Item item,
List<Bitstream> bitstreams, Info info) {
/**
* Counter tracks the position of the bitstream in the list and is used to create the canvas identifier.
* Bitstream order is determined by position in the IIIF DSpace bundle.
*/
int counter = 0;
for (Bitstream bitstream : bitstreams) {
UUID bitstreamID = bitstream.getID();
String mimeType = utils.getBitstreamMimeType(bitstream, context);
if (utils.checkImageMimeType(mimeType)) {
CanvasGenerator canvas = canvasService.getCanvas(item.getID().toString(), info, counter);
addImage(canvas, mimeType, bitstreamID);
if (counter == 2) {
addImage(canvas, mimeType, bitstreamID);
}
sequence.addCanvas(canvas);
counter++;
}
}
}
/**
* A hint to the client as to the most appropriate method of displaying the resource.
*
* @param bitstreamCount count of bitstreams in the IIIF bundle.
*/
private void addViewingHint(int bitstreamCount) {
if (bitstreamCount > 2) {
manifestGenerator.addViewingHint(DOCUMENT_VIEWING_HINT);
}
}
/**
* A link to a machine readable document that semantically describes the resource with
* the seeAlso property, such as an XML or RDF description. This document could be used
* for search and discovery or inferencing purposes, or just to provide a longer
* description of the resource. May have one or more external descriptions related to it.
*
* This method appends an AnnotationList of resources found in the Item's OtherContent bundle.
* A typical use case would be METS or ALTO files that describe the resource.
*
* @param item the DSpace Item.
*/
private void addSeeAlso(Item item) {
List<Bundle> bundles = utils.getBundle(item, OTHER_CONTENT_BUNDLE);
if (bundles.size() == 0) {
return;
}
otherContentGenerator.setIdentifier(IIIF_ENDPOINT + item.getID() + "/manifest/seeAlso");
otherContentGenerator.setType(AnnotationList.TYPE);
otherContentGenerator.setLabel(SEE_ALSO_LABEL);
manifestGenerator.addSeeAlso(otherContentGenerator);
}
/**
* A link to an external resource intended for display or download by a human user.
* This property can be used to link from a manifest, collection or other resource
* to the preferred viewing environment for that resource, such as a viewer page on
* the publishers web site. Other uses include a rendering of a manifest as a PDF
* or EPUB.
*
* This method looks for a PDF rendering in the Item's ORIGINAL bundle and adds
* it to the Sequence if found.
*
* @param sequence Sequence object
* @param item DSpace Item
* @param context DSpace context
*/
private void addRendering(CanvasItemsGenerator sequence, Item item, Context context) {
List<Bundle> bundles = item.getBundles("ORIGINAL");
if (bundles.size() == 0) {
return;
}
Bundle bundle = bundles.get(0);
List<Bitstream> bitstreams = bundle.getBitstreams();
for (Bitstream bitstream : bitstreams) {
String mimeType = null;
try {
mimeType = bitstream.getFormat(context).getMIMEType();
} catch (SQLException e) {
e.printStackTrace();
}
// If the ORIGINAL bundle contains a PDF, assume that it represents the
// item and add to rendering. Ignore other mime-types. This convention should
// be documented.
if (mimeType != null && mimeType.contentEquals("application/pdf")) {
String id = BITSTREAM_PATH_PREFIX + "/" + bitstream.getID() + "/content";
otherContentGenerator.setIdentifier(id);
otherContentGenerator.setLabel(PDF_DOWNLOAD_LABEL);
otherContentGenerator.setFormat(mimeType);
sequence.addRendering(otherContentGenerator);
}
}
}
/**
* A link to a service that makes more functionality available for the resource,
* such as the base URI of an associated IIIF Search API service.
*
* This method returns a search service definition. Search scope is the manifest.
*
* @param item DSpace Item
* @return the IIIF search service definition for the item
*/
private void addSearchService(Item item) {
if (utils.isSearchable(item)) {
contentSearchGenerator.setIdentifier(IIIF_ENDPOINT + item.getID() + "/manifest/search");
// TODO: get label from configuration then set on generator?
manifestGenerator.addService(contentSearchGenerator);
}
}
/**
* Adds Ranges to manifest structures element.
* Ranges are defined in the info.json file.
* @param info
* @param identifier
*/
private void addRanges(Info info, String identifier) {
List<RangeModel> rangesFromConfig = utils.getRangesFromInfoObject(info);
if (rangesFromConfig != null) {
for (int pos = 0; pos < rangesFromConfig.size(); pos++) {
setRange(identifier, rangesFromConfig.get(pos), pos);
manifestGenerator.addRange(rangeGenerator);
}
}
}
/**
* Sets properties on the RangeFacade.
* @param identifier DSpace item id
* @param range range from info.json configuration
* @param pos list position of the range
*/
private void setRange(String identifier, RangeModel range, int pos) {
String id = IIIF_ENDPOINT + identifier + "/r" + pos;
String label = range.getLabel();
rangeGenerator.setIdentifier(id);
rangeGenerator.setLabel(label);
String startCanvas = utils.getCanvasId(range.getStart());
rangeGenerator.addCanvas(canvasService.getRangeCanvasReference(identifier, startCanvas));
}
/**
* Adds thumbnail to the manifest
* @param bitstreams
* @param context
*/
private void addThumbnail(List<Bitstream> bitstreams, Context context) {
if (bitstreams.size() > 0) {
String mimeType = utils.getBitstreamMimeType(bitstreams.get(0), context);
if (utils.checkImageMimeType(mimeType)) {
manifestGenerator.addThumbnail(getThumbnailAnnotation(bitstreams.get(0).getID(), mimeType));
}
}
}
}

View File

@@ -0,0 +1,213 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.apache.commons.io.IOUtils;
import org.dspace.app.rest.iiif.model.generator.AnnotationGenerator;
import org.dspace.app.rest.iiif.model.generator.CanvasGenerator;
import org.dspace.app.rest.iiif.model.generator.ContentAsTextGenerator;
import org.dspace.app.rest.iiif.model.generator.ManifestGenerator;
import org.dspace.app.rest.iiif.model.generator.SearchResultGenerator;
import org.dspace.app.rest.iiif.service.util.IIIFUtils;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
/**
* Implements IIIF Search API queries and responses.
*/
@Component
@RequestScope
public class SearchService extends AbstractResourceService {
@Autowired
IIIFUtils utils;
@Autowired
ContentAsTextGenerator contentAsText;
@Autowired
CanvasGenerator canvas;
@Autowired
AnnotationGenerator annotation;
@Autowired
ManifestGenerator manifest;
@Autowired
SearchResultGenerator searchResult;
public SearchService(ConfigurationService configurationService) {
setConfiguration(configurationService);
}
/**
* Executes a search that is scoped to the manifest.
*
* @param uuid the IIIF manifest uuid
* @param query the solr query
* @return IIIF json
*/
public String searchWithinManifest(UUID uuid, String query) {
String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8);
String json = getSolrSearchResponse(createSearchUrl(encodedQuery, getManifestId(uuid)));
return getAnnotationList(json, uuid, encodedQuery);
}
/**
* Executes the Search API solr query.
* @param url solr query url
* @return json query response
*/
private String getSolrSearchResponse(URL url) {
InputStream jsonStream;
String json = null;
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");
jsonStream = connection.getInputStream();
json = IOUtils.toString(jsonStream, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
return json;
}
/**
* Constructs a solr search URL.
*
* @param encodedQuery the search terms
* @param manifestId the id of the manifest in which to search
* @return solr query
*/
private URL createSearchUrl(String encodedQuery, String manifestId) {
String fullQuery = SEARCH_URL + "/select?" +
"q=ocr_text:\"" + encodedQuery +
"\"%20AND%20manifest_url:\"" + manifestId + "\"" +
"&hl=true" +
"&hl.ocr.fl=ocr_text" +
"&hl.ocr.contextBlock=line" +
"&hl.ocr.contextSize=2" +
"&hl.snippets=10" +
"&hl.ocr.limitBlock=page" +
"&hl.ocr.absoluteHighlights=true";
try {
URL url = new URL(fullQuery);
return url;
} catch (MalformedURLException e) {
throw new RuntimeException("Malformed query URL", e);
}
}
/**
* Generates a Search API response from the word_highlighting solr query response.
*
* The function assumes that the solr query responses contains page IDs
* (taken from the ALTO Page ID element) in the following format:
* Page.0, Page.1, Page.2....
*
* The identifier values must be aligned with zero-based IIIF canvas identifiers:
* c0, c1, c2....
*
* The convention convention for Alto IDs must be followed when indexing ALTO files
* into the word_highlighting solr index. If it is not, search responses will not
* match canvases.
*
* @param json solr search result
* @param uuid DSpace Item uuid
* @param encodedQuery the solr query
* @return a search response in JSON
*/
private String getAnnotationList(String json, UUID uuid, String encodedQuery) {
searchResult.setIdentifier(getManifestId(uuid) + "/search?q=" + encodedQuery);
GsonBuilder builder = new GsonBuilder();
Gson gson = builder.create();
JsonObject body = gson.fromJson(json, JsonObject.class);
// outer ocr highlight element
JsonObject highs = body.getAsJsonObject("ocrHighlighting");
// highlight entries
for (Map.Entry<String, JsonElement> ocrIds: highs.entrySet()) {
// ocr_text
JsonObject ocrObj = ocrIds.getValue().getAsJsonObject().getAsJsonObject("ocr_text");
// snippets array
if (ocrObj != null) {
for (JsonElement snippetArray : ocrObj.getAsJsonObject().get("snippets").getAsJsonArray()) {
for (JsonElement highlights : snippetArray.getAsJsonObject().getAsJsonArray("highlights")) {
for (JsonElement highlight : highlights.getAsJsonArray()) {
JsonObject hcoords = highlight.getAsJsonObject();
String text = (hcoords.get("text").getAsString());
String pageId = getCanvasId((hcoords.get("page").getAsString()));
Integer ulx = hcoords.get("ulx").getAsInt();
Integer uly = hcoords.get("uly").getAsInt();
Integer lrx = hcoords.get("lrx").getAsInt();
Integer lry = hcoords.get("lry").getAsInt();
String w = Integer.toString(lrx - ulx);
String h = Integer.toString(lry - uly);
String params = ulx + "," + uly + "," + w + "," + h;
AnnotationGenerator annot = createSearchResultAnnotation(params, text, pageId, uuid);
searchResult.addResource(annot);
}
}
}
}
}
return utils.asJson(searchResult.getResource());
}
private String getCanvasId(String altoId) {
String[] identArr = altoId.split("\\.");
return "c" + identArr[1];
}
/**
* Creates annotation with word highlight coordinates.
*
* @param params word coordinate parameters used for highlighting.
* @param text word text
* @param pageId the page id returned by solr
* @param uuid the dspace item identifier
* @return a single annotation object that contains word highlights on a single page (canvas)
*/
private AnnotationGenerator createSearchResultAnnotation(String params, String text, String pageId, UUID uuid) {
annotation.setIdentifier(IIIF_ENDPOINT + uuid + "/annot/" + pageId + "-"
+ params);
canvas.setIdentifier(IIIF_ENDPOINT + uuid + "/canvas/" + pageId + "#xywh="
+ params);
annotation.setOnCanvas(canvas);
contentAsText.setText(text);
annotation.setResource(contentAsText);
annotation.setMotivation(AnnotationGenerator.PAINTING);
List<ManifestGenerator> withinList = new ArrayList<>();
manifest.setIdentifier(getManifestId(uuid));
manifest.setLabel("Search within manifest.");
withinList.add(manifest);
annotation.setWithin(withinList);
return annotation;
}
}

View File

@@ -0,0 +1,314 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service.util;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import de.digitalcollections.iiif.model.sharedcanvas.Resource;
import org.apache.log4j.Logger;
import org.dspace.app.rest.iiif.model.ObjectMapperFactory;
import org.dspace.app.rest.iiif.model.info.Info;
import org.dspace.app.rest.iiif.model.info.RangeModel;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.content.BitstreamFormat;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class IIIFUtils {
private static final Logger log = Logger.getLogger(IIIFUtils.class);
// The canvas position will be appended to this string.
private static final String CANVAS_PATH_BASE = "/canvas/c";
// get dbmdz module subclass.
protected SimpleModule iiifModule = ObjectMapperFactory.getIiifModule();
// Use the dbmdz object mapper subclass.
protected ObjectMapper mapper = ObjectMapperFactory.getIiifObjectMapper();
@Autowired
protected BitstreamService bitstreamService;
/**
* For IIIF entities, this method returns the bundle assigned to IIIF
* bitstreams. If the item is not an IIIF entity, the default (ORIGINAL)
* bundle list is returned instead.
* @param item the DSpace item
* @param iiifBundle the name of the IIIF bundle
* @return DSpace bundle
*/
public List<Bundle> getIiifBundle(Item item, String iiifBundle) {
boolean iiif = item.getMetadata().stream()
.filter(m -> m.getMetadataField().toString().contentEquals("relationship_type"))
.anyMatch(m -> m.getValue().contentEquals("IIIF") ||
m.getValue().contentEquals("IIIFSearchable"));
List<Bundle> bundles;
if (iiif) {
bundles = item.getBundles(iiifBundle);
} else {
bundles = item.getBundles();
}
return bundles;
}
/**
* Returns the requested bundle.
* @param item DSpace item
* @param name bundle name
* @return
*/
public List<Bundle> getBundle(Item item, String name) {
return item.getBundles(name);
}
/**
* Returns bitstreams for the first bundle in the list.
* @param bundles list of DSpace bundles
* @return list of bitstreams
*/
public List<Bitstream> getBitstreams(List<Bundle> bundles) {
if (bundles == null || bundles.size() == 0) {
throw new RuntimeException("Unable to retrieve DSpace bundle for manifest.");
}
return bundles.get(0).getBitstreams();
}
/**
* Returns the bitstream mime type
* @param bitstream DSpace bitstream
* @param context DSpace context
* @return mime type
*/
public String getBitstreamMimeType(Bitstream bitstream, Context context) {
try {
BitstreamFormat bitstreamFormat = bitstream.getFormat(context);
return bitstreamFormat.getMIMEType();
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
/**
* Checks to see if the item is searchable. Based on the entity type.
* @param item DSpace item
* @return true if searchable
*/
public boolean isSearchable(Item item) {
return item.getMetadata().stream()
.filter(m -> m.getMetadataField().toString().contentEquals("relationship_type"))
.anyMatch(m -> m.getValue().contentEquals("IIIFSearchable"));
}
/**
* Returns a metadata field name.
* @param meta the DSpace metadata value object
* @return field name as string
*/
public String getMetadataFieldName(MetadataValue meta) {
String element = meta.getMetadataField().getElement();
String qualifier = meta.getMetadataField().getQualifier();
// Need to distinguish DC type from DSpace relationship.type.
// Setting element to be the schema name.
if (meta.getMetadataField().getMetadataSchema().getName().contentEquals("relationship")) {
qualifier = element;
element = meta.getMetadataField().getMetadataSchema().getName();
}
String field = element;
// Add qualifier if defined.
if (qualifier != null) {
field = field + "." + qualifier;
}
return field;
}
/**
* Retrives a bitstream based on its position in the IIIF bundle.
* @param item DSpace Item
* @param canvasPosition bitstream position
* @return bitstream
*/
public Bitstream getBitstreamForCanvas(Item item, String bundleName, int canvasPosition) {
List<Bundle> bundles = item.getBundles(bundleName);
if (bundles.size() == 0) {
return null;
}
List<Bitstream> bitstreams = bundles.get(0).getBitstreams();
try {
return bitstreams.get(canvasPosition);
} catch (RuntimeException e) {
throw new RuntimeException("The requested canvas is not available", e);
}
}
/**
* Attempts to find info.json file in the bitstream bundle and convert
* the json into the Info.class domain model for canvas and range parameters.
* @param context DSpace context
* @param bundleName the IIIF bundle
* @return info domain model
*/
public Info getInfo(Context context, Item item, String bundleName) {
Info info = null;
try {
ObjectMapper mapper = new ObjectMapper();
// Look for expected json file bitstream in bundle.
Bitstream infoBitstream = bitstreamService
.getBitstreamByName(item, bundleName, "info.json");
if (infoBitstream != null) {
InputStream is = bitstreamService.retrieve(context, infoBitstream);
info = mapper.readValue(is, Info.class);
}
} catch (IOException | SQLException e) {
log.warn("Unable to read info.json file.", e);
} catch (AuthorizeException e) {
log.warn("Not authorized to access info.json file.", e);
}
return info;
}
/**
* Returns the range parameter List or null
* @param info the parameters model
* @return list of range models
*/
public List<RangeModel> getRangesFromInfoObject(Info info) {
if (info != null) {
return info.getStructures();
}
return null;
}
/**
* Extracts canvas position from the URL input path.
* @param canvasId e.g. "c12"
* @return the position, e.g. 12
*/
public int getCanvasId(String canvasId) {
return Integer.parseInt(canvasId.substring(1));
}
/**
* Returns the canvas path with position. The path
* returned is partial, not the fully qualified URI.
* @param position position of the bitstream in the DSpace bundle.
* @return partial canvas path.
*/
public String getCanvasId(int position) {
return CANVAS_PATH_BASE + position;
}
/**
* Convenience method to compare canvas parameter and bitstream list size.
* @param info the parameter model
* @param bitstreams the list of DSpace bitstreams
* @return true if sizes match
*/
public boolean isListSizeMatch(Info info, List<Bitstream> bitstreams) {
// If Info is not null then the bitstream bundle contains info.json; exclude
// the file from comparison.
if (info != null && info.getCanvases().size() == bitstreams.size() - 1) {
return true;
}
return false;
}
/**
* Convenience method verifies that the requested canvas exists in the
* parameters model object.
* @param info parameter model
* @param canvasPosition requested canvas position
* @return true if index is in bounds
*/
public boolean canvasOutOfBounds(Info info, int canvasPosition) {
return canvasPosition < 0 || canvasPosition >= info.getCanvases().size();
}
/**
* Validates info.json for a single canvas.
* Unless global settings are being used, when canvas information is not available
* use defaults. The canvas information is defined in the info.json file.
* @param info the information model
* @param position the position of the requested canvas
* @return information model
*/
public Info validateInfoForSingleCanvas(Info info, int position) {
if (info != null && info.getGlobalDefaults() != null) {
if (canvasOutOfBounds(info, position) && !info.getGlobalDefaults().isActivated()) {
log.warn("Canvas for position " + position + " not defined.\n" +
"Ignoring info.json canvas definitions and using defaults. " +
"Any other canvas-level annotations will also be ignored.");
info.setCanvases(new ArrayList<>());
}
}
return info;
}
/**
* Unless global settings are being used, when canvas information list size does
* not match the number of bitstreams use defaults. The canvas information is
* defined in the info.json file.
* @param info the information model
* @param bitstreams the list of bitstreams
* @return information model
*/
public Info validateInfoForManifest(Info info, List<Bitstream> bitstreams) {
if (info != null && info.getGlobalDefaults() != null) {
if (!isListSizeMatch(info, bitstreams) && !info.getGlobalDefaults().isActivated()) {
log.warn("Mismatch between info.json canvases and DSpace bitstream count.\n" +
"Ignoring info.json canvas definitions and using defaults." +
"Any other canvas-level annotations will also be ignored.");
info.setCanvases(new ArrayList<>());
}
}
return info;
}
/**
* Serializes the json response.
* @param resource to be serialized
* @return
*/
public String asJson(Resource<?> resource) {
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.registerModule(iiifModule);
try {
return mapper.writeValueAsString(resource);
} catch (JsonProcessingException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* Tests for image mimetype. Presentation API 2.1.1 canvas supports images only.
* Other media types introduced in version 3.
* @param mimetype
* @return true if an image
*/
public boolean checkImageMimeType(String mimetype) {
if (mimetype != null && mimetype.contains("image/")) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,33 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service.util;
import org.dspace.app.rest.iiif.model.generator.ProfileGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ImageProfileUtil {
@Autowired
ProfileGenerator profile;
/**
* Utility method for obtaining the image service profile.
* Calling from this utility provides a unique instance of the
* autowired property. Necessary because a single canvas resource contains
* both thumbnail and images.
*
* @return image service profile
*/
public ProfileGenerator getImageProfile() throws
RuntimeException {
profile.setIdentifier("http://iiif.io/api/image/2/level1.json");
return profile;
}
}

View File

@@ -0,0 +1,34 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.iiif.service.util;
import org.dspace.app.rest.iiif.model.generator.ProfileGenerator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ThumbProfileUtil {
@Autowired
ProfileGenerator profile;
/**
* Utility method for obtaining the thumbnail image service profile.
* Calling from this utility provides a unique instance of the
* autowired property. Necessary because a single canvas resource contains
* both thumbnail and images.
*
* @return the thumbnail service profile
*/
public ProfileGenerator getThumbnailProfile() throws
RuntimeException {
profile.setIdentifier("http://iiif.io/api/image/2/level0.json");
return profile;
}
}