From 1a6d3d4ee3e6572ad09217246239247c0213dade Mon Sep 17 00:00:00 2001 From: Michael Spalti Date: Thu, 25 Mar 2021 15:52:21 -0700 Subject: [PATCH] Added iiif service and utility classes. --- .../iiif/service/AbstractResourceService.java | 157 ++++++++ .../iiif/service/AnnotationListService.java | 121 +++++++ .../iiif/service/CanvasLookupService.java | 59 +++ .../app/rest/iiif/service/CanvasService.java | 93 +++++ .../rest/iiif/service/ManifestService.java | 338 ++++++++++++++++++ .../app/rest/iiif/service/SearchService.java | 213 +++++++++++ .../app/rest/iiif/service/util/IIIFUtils.java | 314 ++++++++++++++++ .../iiif/service/util/ImageProfileUtil.java | 33 ++ .../iiif/service/util/ThumbProfileUtil.java | 34 ++ 9 files changed, 1362 insertions(+) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/AbstractResourceService.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/AnnotationListService.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/CanvasLookupService.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/CanvasService.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/ManifestService.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/SearchService.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/IIIFUtils.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/ImageProfileUtil.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/ThumbProfileUtil.java diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/AbstractResourceService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/AbstractResourceService.java new file mode 100644 index 0000000000..6b87cae231 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/AbstractResourceService.java @@ -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 service’s 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; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/AnnotationListService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/AnnotationListService.java new file mode 100644 index 0000000000..43e7caf653 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/AnnotationListService.java @@ -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 bundles = utils.getBundle(item, OTHER_CONTENT_BUNDLE); + if (bundles.size() > 0) { + for (Bundle bundle : bundles) { + List 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()); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/CanvasLookupService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/CanvasLookupService.java new file mode 100644 index 0000000000..186a3de7cb --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/CanvasLookupService.java @@ -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 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()); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/CanvasService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/CanvasService.java new file mode 100644 index 0000000000..fe1f30427a --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/CanvasService.java @@ -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; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/ManifestService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/ManifestService.java new file mode 100644 index 0000000000..ec63cd750d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/ManifestService.java @@ -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 bundles = utils.getIiifBundle(item, IIIF_BUNDLE); + List 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 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 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 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 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 publisher’s 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 bundles = item.getBundles("ORIGINAL"); + if (bundles.size() == 0) { + return; + } + Bundle bundle = bundles.get(0); + List 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 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 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)); + } + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/SearchService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/SearchService.java new file mode 100644 index 0000000000..a16e0e32ab --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/SearchService.java @@ -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 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 withinList = new ArrayList<>(); + manifest.setIdentifier(getManifestId(uuid)); + manifest.setLabel("Search within manifest."); + withinList.add(manifest); + annotation.setWithin(withinList); + return annotation; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/IIIFUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/IIIFUtils.java new file mode 100644 index 0000000000..e41815c445 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/IIIFUtils.java @@ -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 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 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 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 getBitstreams(List 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 bundles = item.getBundles(bundleName); + if (bundles.size() == 0) { + return null; + } + List 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 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 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 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; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/ImageProfileUtil.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/ImageProfileUtil.java new file mode 100644 index 0000000000..63fa43910e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/ImageProfileUtil.java @@ -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; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/ThumbProfileUtil.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/ThumbProfileUtil.java new file mode 100644 index 0000000000..58e5dd18b3 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/iiif/service/util/ThumbProfileUtil.java @@ -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; + } + +}