Finalize configuration via metadata refactoring and hierarchical range support

This commit is contained in:
Andrea Bollini
2021-09-26 22:59:38 +02:00
parent 5916f610b3
commit 21032f66a5
18 changed files with 913 additions and 122 deletions

View File

@@ -6,6 +6,9 @@
# http://www.dspace.org/license/
#
iiif.canvas.default-naming = Page
iiif.toc.root-label = Table of Contents
itemlist.dc.contributor.* = Author(s)
itemlist.dc.contributor.author = Author(s)
itemlist.dc.creator = Author(s)
@@ -38,6 +41,14 @@ metadata.dc.relation.ispartofseries = Series/Report no.
metadata.dc.subject = Keywords
metadata.dc.title = Title
metadata.dc.title.alternative = Other Titles
metadata.bitstream.dc.title = File name
metadata.bitstream.dc.description = Description
metadata.bitstream.iiif.image.width = Image Width (px)
metadata.bitstream.iiif.image.height= Image Height (px)
metadata.bitstream.iiif-virtual.format = Format
metadata.bitstream.iiif-virtual.mimetype = Mime Type
metadata.bitstream.iiif-virtual.bytes = File size
metadata.bitstream.iiif-virtual.checksum = Checksum
org.dspace.app.itemexport.no-result = The DSpaceObject that you specified has no items.
org.dspace.checker.ResultsLogger.bitstream-format = Bitstream format

View File

@@ -127,6 +127,10 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return addMetadataValue(item, "iiif", "search", "enabled", "true");
}
public ItemBuilder withIIIFViewingHint(String hint) {
return addMetadataValue(item, "iiif", "viewing", "hint", hint);
}
public ItemBuilder withIIIFCanvasNaming(String naming) {
return addMetadataValue(item, "iiif", "canvas", "naming", naming);
}

View File

@@ -11,6 +11,7 @@ import java.util.ArrayList;
import java.util.List;
import de.digitalcollections.iiif.model.ImageContent;
import de.digitalcollections.iiif.model.MetadataEntry;
import de.digitalcollections.iiif.model.sharedcanvas.Canvas;
import de.digitalcollections.iiif.model.sharedcanvas.Resource;
@@ -29,6 +30,8 @@ public class CanvasGenerator implements IIIFResource {
private List<ImageContent> images = new ArrayList();
private ImageContent thumbnail;
private final List<MetadataEntry> metadata = new ArrayList<>();
public CanvasGenerator setIdentifier(String identifier) {
this.identifier = identifier;
return this;
@@ -83,6 +86,18 @@ public class CanvasGenerator implements IIIFResource {
return this;
}
/**
* Adds single metadata field to Manifest.
* @param field property field
* @param value property value
*/
public void addMetadata(String field, String value, String... rest) {
MetadataEntryGenerator metadataEntryGenerator = new MetadataEntryGenerator();
metadataEntryGenerator.setField(field);
metadataEntryGenerator.setValue(value, rest);
metadata.add(metadataEntryGenerator.getValue());
}
/**
* Returns the canvas.
* @return canvas model
@@ -114,6 +129,11 @@ public class CanvasGenerator implements IIIFResource {
canvas.addThumbnail(thumbnail);
}
}
if (metadata.size() > 0) {
for (MetadataEntry meta : metadata) {
canvas.addMetadata(meta);
}
}
return canvas;
}

View File

@@ -32,7 +32,7 @@ import org.springframework.web.context.annotation.RequestScope;
public class CanvasItemsGenerator implements org.dspace.app.rest.iiif.model.generator.IIIFResource {
private String identifier;
private OtherContent rendering;
private final List<OtherContent> renderings = new ArrayList<>();
private final List<Canvas> canvas = new ArrayList<>();
@Autowired
@@ -53,8 +53,7 @@ public class CanvasItemsGenerator implements org.dspace.app.rest.iiif.model.gene
* @param otherContent wrapper for OtherContent
*/
public void addRendering(org.dspace.app.rest.iiif.model.generator.ExternalLinksGenerator otherContent) {
this.rendering = (OtherContent) otherContent.getResource();
this.renderings.add((OtherContent) otherContent.getResource());
}
/**
@@ -70,8 +69,8 @@ public class CanvasItemsGenerator implements org.dspace.app.rest.iiif.model.gene
@Override
public Resource<Sequence> getResource() {
Sequence items = new Sequence(identifier);
if (rendering != null) {
items.addRendering(rendering);
for (OtherContent r : renderings) {
items.addRendering(r);
}
items.setCanvases(canvas);
return items;

View File

@@ -31,6 +31,9 @@ import org.springframework.web.context.annotation.RequestScope;
* such as a title and other descriptive information about the object or the intellectual work that
* it conveys. Each manifest describes how to present a single object such as a book, a photograph,
* or a statue.
*
* Please note that this is a request scoped bean. This mean that for each http request a
* different instance will be initialized by Spring and used to serve this specific request.
*/
@Component
@RequestScope
@@ -48,7 +51,7 @@ public class ManifestGenerator implements IIIFResource {
private ContentSearchService searchService;
private final List<URI> license = new ArrayList<>();
private final List<MetadataEntry> metadata = new ArrayList<>();
private List<RangeGenerator> ranges = new ArrayList<>();
private final List<RangeGenerator> ranges = new ArrayList<>();
@Autowired
MetadataEntryGenerator metadataEntryGenerator;

View File

@@ -9,6 +9,7 @@ package org.dspace.app.rest.iiif.model.generator;
import de.digitalcollections.iiif.model.MetadataEntry;
import de.digitalcollections.iiif.model.PropertyValue;
import org.dspace.core.I18nUtil;
import org.springframework.stereotype.Component;
@Component
@@ -43,6 +44,6 @@ public class MetadataEntryGenerator implements IIIFValue {
} else {
metadataValues = new PropertyValue(value);
}
return new MetadataEntry(new PropertyValue(field), metadataValues);
return new MetadataEntry(new PropertyValue(I18nUtil.getMessage("metadata." + field)), metadataValues);
}
}

View File

@@ -46,8 +46,8 @@ public class CanvasLookupService extends AbstractResourceService {
String mimeType = utils.getBitstreamMimeType(bitstream, context);
CanvasGenerator canvasGenerator;
try {
canvasGenerator = canvasService.getCanvas(item.getID().toString(), bitstream, bitstream.getBundles().get(0),
item, canvasPosition, mimeType);
canvasGenerator = canvasService.getCanvas(context, item.getID().toString(), bitstream,
bitstream.getBundles().get(0), item, canvasPosition, mimeType);
} catch (SQLException e) {
throw new RuntimeException(e);
}

View File

@@ -7,17 +7,27 @@
*/
package org.dspace.app.rest.iiif.service;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.iiif.model.generator.CanvasGenerator;
import org.dspace.app.rest.iiif.model.generator.ImageContentGenerator;
import org.dspace.app.rest.iiif.service.util.BitstreamIIIFVirtualMetadata;
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.factory.ContentServiceFactory;
import org.dspace.content.service.BitstreamService;
import org.dspace.core.Context;
import org.dspace.core.I18nUtil;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -33,6 +43,11 @@ public class CanvasService extends AbstractResourceService {
@Autowired
IIIFUtils utils;
@Autowired
ApplicationContext applicationContext;
protected String[] BITSTREAM_METADATA_FIELDS;
/**
* Constructor.
*
@@ -40,6 +55,7 @@ public class CanvasService extends AbstractResourceService {
*/
public CanvasService(ConfigurationService configurationService) {
setConfiguration(configurationService);
BITSTREAM_METADATA_FIELDS = configurationService.getArrayProperty("iiif.metadata.bitstream");
}
/**
@@ -50,6 +66,7 @@ public class CanvasService extends AbstractResourceService {
* Note that info.json is going to be replaced with metadata in the bitstream
* DSO.
*
* @param context DSpace Context
* @param manifestId manifest id
* @param bitstreamId uuid of the bitstream
* @param mimeType the mimetype of the bitstream
@@ -57,11 +74,11 @@ public class CanvasService extends AbstractResourceService {
* @param count the canvas position in the sequence.
* @return canvas object
*/
protected CanvasGenerator getCanvas(String manifestId, Bitstream bitstream, Bundle bundle, Item item, int count,
String mimeType) {
protected CanvasGenerator getCanvas(Context context, String manifestId, Bitstream bitstream, Bundle bundle,
Item item, int count, String mimeType) {
int pagePosition = count + 1;
String canvasNaming = utils.getCanvasNaming(item, "Page");
String canvasNaming = utils.getCanvasNaming(item, I18nUtil.getMessage("iiif.canvas.default-naming"));
String label = utils.getIIIFLabel(bitstream, canvasNaming + " " + pagePosition);
int canvasWidth = utils.getCanvasWidth(bitstream, bundle, item, DEFAULT_CANVAS_WIDTH);
int canvasHeight = utils.getCanvasHeight(bitstream, bundle, item, DEFAULT_CANVAS_HEIGHT);
@@ -73,9 +90,10 @@ public class CanvasService extends AbstractResourceService {
ImageContentGenerator thumb = imageContentService.getImageContent(bitstreamId, mimeType,
thumbUtil.getThumbnailProfile(), THUMBNAIL_PATH);
return new CanvasGenerator().setIdentifier(IIIF_ENDPOINT + manifestId + "/canvas/c" + count)
.addImage(image.getResource()).addThumbnail(thumb.getResource()).setHeight(canvasHeight)
.setWidth(canvasWidth).setLabel(label);
return addMetadata(context, bitstream,
new CanvasGenerator().setIdentifier(IIIF_ENDPOINT + manifestId + "/canvas/c" + count)
.addImage(image.getResource()).addThumbnail(thumb.getResource()).setHeight(canvasHeight)
.setWidth(canvasWidth).setLabel(label));
}
/**
@@ -89,4 +107,55 @@ public class CanvasService extends AbstractResourceService {
return new CanvasGenerator().setIdentifier(IIIF_ENDPOINT + identifier + startCanvas);
}
/**
* Adds DSpace bitstream metadata to the canvas.
*
* @param context the DSpace Context
* @param item the DSpace item
*/
private CanvasGenerator addMetadata(Context context, Bitstream bitstream, CanvasGenerator canvasGenerator) {
BitstreamService bService = ContentServiceFactory.getInstance().getBitstreamService();
for (String field : BITSTREAM_METADATA_FIELDS) {
if (StringUtils.startsWith(field, "@") && StringUtils.endsWith(field, "@")) {
String virtualFieldName = field.substring(1, field.length() - 1);
String beanName = BitstreamIIIFVirtualMetadata.IIIF_BITSTREAM_VIRTUAL_METADATA_BEAN_PREFIX +
virtualFieldName;
BitstreamIIIFVirtualMetadata virtual = applicationContext.getBean(beanName,
BitstreamIIIFVirtualMetadata.class);
List<String> values = virtual.getValues(context, bitstream);
if (values.size() > 0) {
if (values.size() > 1) {
canvasGenerator.addMetadata("bitstream.iiif-virtual." + virtualFieldName, values.get(0),
values.subList(1, values.size()).toArray(new String[values.size() - 1]));
} else {
canvasGenerator.addMetadata("bitstream.iiif-virtual." + virtualFieldName, values.get(0));
}
}
} else {
String[] eq = field.split("\\.");
String schema = eq[0];
String element = eq[1];
String qualifier = null;
if (eq.length > 2) {
qualifier = eq[2];
}
List<MetadataValue> metadata = bService.getMetadata(bitstream, schema, element, qualifier,
Item.ANY);
List<String> values = new ArrayList<String>();
for (MetadataValue meta : metadata) {
values.add(meta.getValue());
}
if (values.size() > 0) {
if (values.size() > 1) {
canvasGenerator.addMetadata("bitstream." + field, values.get(0),
values.subList(1, values.size()).toArray(new String[values.size() - 1]));
} else {
canvasGenerator.addMetadata("bitstream." + field, values.get(0));
}
}
}
}
return canvasGenerator;
}
}

View File

@@ -9,7 +9,7 @@ package org.dspace.app.rest.iiif.service;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -28,13 +28,22 @@ import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.core.I18nUtil;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
/**
* Generates IIIF Manifest JSON response for a DSpace Item.
* Generates IIIF Manifest JSON response for a DSpace Item. Please note that
* this is a request scoped bean. This mean that for each http request a
* different instance will be initialized by Spring and used to serve this
* specific request. This is needed because some configuration are cached in the
* instance. Moreover, many injected dependencies are also request scoped or
* prototype (that will turn in a request scope when injected in a request scope
* bean). The generators need to be request scoped as they act as builder
* storing the object state during each incremental building step until the
* final object is returned (IIIF Resource)
*/
@Component
@RequestScope
@@ -105,9 +114,6 @@ public class ManifestService extends AbstractResourceService {
* @return manifest object
*/
private void populateManifest(Item item, Context context) {
// If an IIIF bundle is found it will be used. Otherwise,
// images in the ORIGINAL bundle will be used.
List<Bundle> bundles = utils.getIiifBundles(item);
String manifestId = getManifestId(item.getID());
manifestGenerator.setIdentifier(manifestId);
manifestGenerator.setLabel(item.getName());
@@ -117,20 +123,35 @@ public class ManifestService extends AbstractResourceService {
addMetadata(context, item);
addViewingHint(item);
addThumbnail(item, context);
addRanges(context, item, manifestId);
manifestGenerator.addSequence(
sequenceService.getSequence(item, context));
addSeeAlso(item);
}
/**
* Add the ranges to the manifest structure. Ranges are generated from the
* iiif.toc metadata
*
* @param context the DSpace Context
* @param item the DSpace Item to represent
* @param manifestId the generated manifestId
*/
private void addRanges(Context context, Item item, String manifestId) {
List<Bundle> bundles = utils.getIIIFBundles(item);
RangeGenerator root = new RangeGenerator(rangeService);
root.setLabel("Table of Contents");
root.setLabel(I18nUtil.getMessage("iiif.toc.root-label"));
root.setIdentifier(manifestId + "/range/r0");
manifestGenerator.addRange(root);
// manifestGenerator.addRange(root);
Map<String, RangeGenerator> tocRanges = new HashMap<String, RangeGenerator>();
Map<String, RangeGenerator> tocRanges = new LinkedHashMap<String, RangeGenerator>();
for (Bundle bnd : bundles) {
String bundleToCPrefix = null;
if (bundles.size() > 1) {
bundleToCPrefix = utils.getIIIFFirstToC(bnd);
bundleToCPrefix = utils.getBundleIIIFToC(bnd);
}
RangeGenerator lastRange = root;
for (Bitstream b : utils.getIiifBitstreams(context, bnd)) {
for (Bitstream b : utils.getIIIFBitstreams(context, bnd)) {
CanvasGenerator canvasId = sequenceService.addCanvas(context, item, bnd, b);
List<String> tocs = utils.getIIIFToCs(b, bundleToCPrefix);
if (tocs.size() > 0) {
@@ -153,7 +174,7 @@ public class ManifestService extends AbstractResourceService {
currRange.addSubRange(range);
// add the range to the manifest
manifestGenerator.addRange(range);
// manifestGenerator.addRange(range);
// move the current range
currRange = range;
@@ -170,10 +191,12 @@ public class ManifestService extends AbstractResourceService {
}
}
}
manifestGenerator.addSequence(
sequenceService.getSequence(item, context));
addSeeAlso(item);
if (tocRanges.size() > 0) {
manifestGenerator.addRange(root);
for (RangeGenerator range : tocRanges.values()) {
manifestGenerator.addRange(range);
}
}
}
/**
@@ -216,15 +239,15 @@ public class ManifestService extends AbstractResourceService {
manifestGenerator.addMetadata(field, values.get(0));
}
}
String descrValue = item.getItemService().getMetadataFirstValue(item, "dc", "description", null, Item.ANY);
if (StringUtils.isNotBlank(descrValue)) {
manifestGenerator.addDescription(descrValue);
}
}
String descrValue = item.getItemService().getMetadataFirstValue(item, "dc", "description", null, Item.ANY);
if (StringUtils.isNotBlank(descrValue)) {
manifestGenerator.addDescription(descrValue);
}
String licenseUriValue = item.getItemService().getMetadataFirstValue(item, "dc", "rights", "uri", Item.ANY);
if (StringUtils.isNotBlank(licenseUriValue)) {
manifestGenerator.addLicense(licenseUriValue);
}
String licenseUriValue = item.getItemService().getMetadataFirstValue(item, "dc", "rights", "uri", Item.ANY);
if (StringUtils.isNotBlank(licenseUriValue)) {
manifestGenerator.addLicense(licenseUriValue);
}
}
@@ -288,7 +311,7 @@ public class ManifestService extends AbstractResourceService {
* @param context DSpace context
*/
private void addThumbnail(Item item, Context context) {
List<Bitstream> bitstreams = utils.getIiifBitstreams(context, item);
List<Bitstream> bitstreams = utils.getIIIFBitstreams(context, item);
if (bitstreams != null && bitstreams.size() > 0) {
String mimeType = utils.getBitstreamMimeType(bitstreams.get(0), context);
ImageContentGenerator image = imageContentService

View File

@@ -9,7 +9,6 @@ package org.dspace.app.rest.iiif.service;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.iiif.model.generator.CanvasGenerator;
@@ -26,13 +25,10 @@ import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class SequenceService extends AbstractResourceService {
public class SequenceService extends AbstractResourceService {
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SequenceService.class);
// TODO i18n
private static final String PDF_DOWNLOAD_LABEL = "Download as PDF";
/*
* The counter tracks the position of the bitstream in the list and is used to create the canvas identifier.
* The order of bitstreams (and thus page order in documents) is determined by position in the DSpace
@@ -49,7 +45,6 @@ public class SequenceService extends AbstractResourceService {
@Autowired
CanvasService canvasService;
public SequenceService(ConfigurationService configurationService) {
setConfiguration(configurationService);
}
@@ -71,12 +66,11 @@ public class SequenceService extends AbstractResourceService {
* @param bitstreams list of DSpace bitstreams
*/
public CanvasGenerator addCanvas(Context context, Item item, Bundle bnd, Bitstream bitstream) {
UUID bitstreamId = bitstream.getID();
String mimeType = utils.getBitstreamMimeType(bitstream, context);
String manifestId = item.getID().toString();
CanvasGenerator canvasGenerator =
canvasService.getCanvas(manifestId, bitstream, bnd, item, counter, mimeType);
String canvasIdentifier = sequenceGenerator.addCanvas(canvasGenerator);
canvasService.getCanvas(context, manifestId, bitstream, bnd, item, counter, mimeType);
sequenceGenerator.addCanvas(canvasGenerator);
counter++;
return canvasGenerator;
}
@@ -95,32 +89,31 @@ public class SequenceService extends AbstractResourceService {
* @param context DSpace context
*/
private void addRendering(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. Other options
// might be using the primary bitstream or relying on a bitstream metadata
// field, e.g. iiif.rendering
if (mimeType != null && mimeType.contentEquals("application/pdf")) {
String id = BITSTREAM_PATH_PREFIX + "/" + bitstream.getID() + "/content";
sequenceGenerator.addRendering(
externalLinksGenerator
.setIdentifier(id)
.setLabel(PDF_DOWNLOAD_LABEL)
.setFormat(mimeType)
);
List<Bundle> bundles = utils.getIIIFBundles(item);
for (Bundle bundle : bundles) {
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 bundle contains a PDF, assume that it represents the
// item and add to rendering. Ignore other mime-types. Other options
// might be using the primary bitstream or relying on a bitstream metadata
// field, e.g. iiif.rendering
if (mimeType != null && mimeType.contentEquals("application/pdf")) {
String id = BITSTREAM_PATH_PREFIX + "/" + bitstream.getID() + "/content";
sequenceGenerator.addRendering(
externalLinksGenerator
.setIdentifier(id)
.setLabel(utils.getIIIFLabel(bitstream, bitstream.getName()))
.setFormat(mimeType)
);
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
/**
* 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.util.Collections;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.dspace.content.Bitstream;
import org.dspace.core.Context;
import org.springframework.stereotype.Component;
/**
* Expose the Bitstream file size as a IIIF Metadata
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
@Component(BitstreamIIIFVirtualMetadata.IIIF_BITSTREAM_VIRTUAL_METADATA_BEAN_PREFIX + "bytes")
public class BitstreamBytesIIIFVirtualMetadata implements BitstreamIIIFVirtualMetadata {
@Override
public List<String> getValues(Context context, Bitstream bitstream) {
return Collections.singletonList(FileUtils.byteCountToDisplaySize(bitstream.getSizeBytes()));
}
}

View File

@@ -0,0 +1,30 @@
/**
* 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.util.Collections;
import java.util.List;
import org.dspace.content.Bitstream;
import org.dspace.core.Context;
import org.springframework.stereotype.Component;
/**
* Expose the Bitstream Checksum as a IIIF Metadata
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
@Component(BitstreamIIIFVirtualMetadata.IIIF_BITSTREAM_VIRTUAL_METADATA_BEAN_PREFIX + "checksum")
public class BitstreamChecksumIIIFVirtualMetadata implements BitstreamIIIFVirtualMetadata {
@Override
public List<String> getValues(Context context, Bitstream bitstream) {
return Collections.singletonList(bitstream.getChecksum() + " (" + bitstream.getChecksumAlgorithm() + ")");
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.sql.SQLException;
import java.util.Collections;
import java.util.List;
import org.dspace.content.Bitstream;
import org.dspace.core.Context;
import org.springframework.stereotype.Component;
/**
* Expose the Bitstream Format as a IIIF Metadata
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
@Component(BitstreamIIIFVirtualMetadata.IIIF_BITSTREAM_VIRTUAL_METADATA_BEAN_PREFIX + "format")
public class BitstreamFormatIIIFVirtualMetadata implements BitstreamIIIFVirtualMetadata {
@Override
public List<String> getValues(Context context, Bitstream bitstream) {
try {
return Collections.singletonList(bitstream.getFormatDescription(context));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* 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.util.List;
import org.dspace.content.Bitstream;
import org.dspace.core.Context;
/**
* Interface to implement to expose additional information at the canvas level
* for the bitstream
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public interface BitstreamIIIFVirtualMetadata {
public final String IIIF_BITSTREAM_VIRTUAL_METADATA_BEAN_PREFIX = "iiif.bitstream.";
List<String> getValues(Context context, Bitstream bitstream);
}

View File

@@ -0,0 +1,35 @@
/**
* 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.sql.SQLException;
import java.util.Collections;
import java.util.List;
import org.dspace.content.Bitstream;
import org.dspace.core.Context;
import org.springframework.stereotype.Component;
/**
* Expose the Bitstream format mime type as a IIIF Metadata
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
@Component(BitstreamIIIFVirtualMetadata.IIIF_BITSTREAM_VIRTUAL_METADATA_BEAN_PREFIX + "mimetype")
public class BitstreamMimetypeIIIFVirtualMetadata implements BitstreamIIIFVirtualMetadata {
@Override
public List<String> getValues(Context context, Bitstream bitstream) {
try {
return Collections.singletonList(bitstream.getFormat(context).getMIMEType());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -38,20 +38,34 @@ public class IIIFUtils {
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(IIIFUtils.class);
// The DSpace bundle for other content related to item.
protected static final String OTHER_CONTENT_BUNDLE = "OtherContent";
// The canvas position will be appended to this string.
private static final String CANVAS_PATH_BASE = "/canvas/c";
// metadata used to enable the iiif features on the item
public static final String METADATA_IIIF_ENABLED = "dspace.iiif.enabled";
// metadata used to enable the iiif search service on the item
public static final String METADATA_IIIF_SEARCH_ENABLED = "iiif.search.enabled";
// metadata used to override the title/name exposed as label to iiif client
public static final String METADATA_IIIF_LABEL = "iiif.label";
// metadata used to override the description/abstract exposed as label to iiif client
public static final String METADATA_IIIF_DESCRIPTION = "iiif.description";
// metadata used to set the position of the resource in the iiif manifest structure
public static final String METADATA_IIIF_TOC = "iiif.toc";
// metadata used to set the naming convention (prefix) used for all canvas that has not an explicit name
public static final String METADATA_IIIF_CANVAS_NAMING = "iiif.canvas.naming";
// metadata used to set the iiif viewing hint
public static final String METADATA_IIIF_VIEWING_HINT = "iiif.viewing.hint";
// metadata used to set the width of the canvas that has not an explicit name
public static final String METADATA_IMAGE_WIDTH = "iiif.image.width";
// metadata used to set the height of the canvas that has not an explicit name
public static final String METADATA_IMAGE_HEIGTH = "iiif.image.height";
// string used in the metadata toc as separator among the different levels
public static final String TOC_SEPARATOR = "|||";
// convenient constant to split a toc in its components
public static final String TOC_SEPARATOR_REGEX = "\\|\\|\\|";
// get module subclass.
@@ -69,7 +83,7 @@ public class IIIFUtils {
*
* @return list of DSpace bundles with IIIF content
*/
public List<Bundle> getIiifBundles(Item item) {
public List<Bundle> getIIIFBundles(Item item) {
boolean iiif = isIIIFEnabled(item);
List<Bundle> bundles = new ArrayList<>();
if (iiif) {
@@ -78,6 +92,12 @@ public class IIIFUtils {
return bundles;
}
/**
* This method verify if the IIIF feature is enabled on the item
*
* @param item the dspace item
* @return true if the item supports IIIF
*/
public boolean isIIIFEnabled(Item item) {
return item.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(METADATA_IIIF_ENABLED))
@@ -85,6 +105,13 @@ public class IIIFUtils {
m.getValue().equalsIgnoreCase("yes"));
}
/**
* Utility method to check is a bundle can contain bitstreams to use as IIIF
* resources
*
* @param b the DSpace bundle to check
* @return true if the bundle can contain bitstreams to use as IIIF resources
*/
private boolean isIIIFBundle(Bundle b) {
return !StringUtils.equalsAnyIgnoreCase(b.getName(), Constants.LICENSE_BUNDLE_NAME,
Constants.METADATA_BUNDLE_NAME, CreativeCommonsServiceImpl.CC_BUNDLE_NAME, "THUMBNAIL",
@@ -94,21 +121,43 @@ public class IIIFUtils {
.noneMatch(m -> m.getValue().equalsIgnoreCase("false") || m.getValue().equalsIgnoreCase("no"));
}
public List<Bitstream> getIiifBitstreams(Context context, Item item) {
/**
* Return all the bitstreams in the item to be used as IIIF resources
*
* @param context the DSpace Context
* @param item the DSpace item
* @return a not null list of bitstreams to use as IIIF resources in the
* manifest
*/
public List<Bitstream> getIIIFBitstreams(Context context, Item item) {
List<Bitstream> bitstreams = new ArrayList<Bitstream>();
for (Bundle bnd : getIiifBundles(item)) {
for (Bundle bnd : getIIIFBundles(item)) {
bitstreams
.addAll(getIiifBitstreams(context, bnd));
.addAll(getIIIFBitstreams(context, bnd));
}
return bitstreams;
}
public List<Bitstream> getIiifBitstreams(Context context, Bundle bundle) {
return bundle.getBitstreams().stream().filter(b -> isIiifBitstream(context, b))
/**
* Return all the bitstreams in the bundle to be used as IIIF resources
*
* @param context the DSpace Context
* @param item the DSpace Bundle
* @return a not null list of bitstreams to use as IIIF resources in the
* manifest
*/
public List<Bitstream> getIIIFBitstreams(Context context, Bundle bundle) {
return bundle.getBitstreams().stream().filter(b -> isIIIFBitstream(context, b))
.collect(Collectors.toList());
}
private boolean isIiifBitstream(Context context, Bitstream b) {
/**
* Utility method to check is a bitstream can be used as IIIF resources
*
* @param b the DSpace bitstream to check
* @return true if the bitstream can be used as IIIF resource
*/
private boolean isIIIFBitstream(Context context, Bitstream b) {
return checkImageMimeType(getBitstreamMimeType(b, context)) && b.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(METADATA_IIIF_ENABLED))
.noneMatch(m -> m.getValue().equalsIgnoreCase("false") || m.getValue().equalsIgnoreCase("no"));
@@ -116,8 +165,9 @@ public class IIIFUtils {
/**
* Returns the bitstream mime type
*
* @param bitstream DSpace bitstream
* @param context DSpace context
* @param context DSpace context
* @return mime type
*/
public String getBitstreamMimeType(Bitstream bitstream, Context context) {
@@ -131,7 +181,9 @@ public class IIIFUtils {
}
/**
* Checks to see if the item is searchable. Based on the {@link #METADATA_IIIF_SEARCH_ENABLED} metadata.
* Checks to see if the item is searchable. Based on the
* {@link #METADATA_IIIF_SEARCH_ENABLED} metadata.
*
* @param item DSpace item
* @return true if the iiif search is enabled
*/
@@ -144,18 +196,16 @@ public class IIIFUtils {
/**
* Retrives a bitstream based on its position in the IIIF bundle.
* @param context DSpace Context
* @param item DSpace Item
*
* @param context DSpace Context
* @param item DSpace Item
* @param canvasPosition bitstream position
* @return bitstream
* @return bitstream or null if the specified canvasPosition doesn't exist in
* the manifest
*/
public Bitstream getBitstreamForCanvas(Context context, Item item, int canvasPosition) {
List<Bitstream> bitstreams = getIiifBitstreams(context, item);
try {
return bitstreams.size() > canvasPosition ? bitstreams.get(canvasPosition) : null;
} catch (RuntimeException e) {
throw new RuntimeException("The requested canvas is not available", e);
}
List<Bitstream> bitstreams = getIIIFBitstreams(context, item);
return bitstreams.size() > canvasPosition ? bitstreams.get(canvasPosition) : null;
}
/**
@@ -205,24 +255,67 @@ public class IIIFUtils {
return false;
}
/**
* Return all the bitstreams in the item to be used as annotations
*
* @param context the DSpace Context
* @param item the DSpace item
* @return a not null list of bitstreams to use as IIIF resources in the
* manifest
*/
public List<Bitstream> getSeeAlsoBitstreams(Item item) {
return new ArrayList<Bitstream>();
List<Bitstream> seeAlsoBitstreams = new ArrayList<Bitstream>();
List<Bundle> bundles = item.getBundles(OTHER_CONTENT_BUNDLE);
if (bundles.size() > 0) {
for (Bundle bundle : bundles) {
List<Bitstream> bitstreams = bundle.getBitstreams();
seeAlsoBitstreams.addAll(bitstreams);
}
}
return seeAlsoBitstreams;
}
/**
* Return the custom iiif label for the resource or the provided default if none
*
* @param dso the dspace object to use as iiif resource
* @param defaultLabel the default label to return if none is specified in the
* metadata
* @return the iiif label for the dspace object
*/
public String getIIIFLabel(DSpaceObject dso, String defaultLabel) {
return dso.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(METADATA_IIIF_LABEL))
.findFirst().map(m -> m.getValue()).orElse(defaultLabel);
}
/**
* Return the custom iiif description for the resource or the provided default if none
*
* @param dso the dspace object to use as iiif resource
* @param defaultLabel the default description to return if none is specified in the
* metadata
* @return the iiif label for the dspace object
*/
public String getIIIFDescription(DSpaceObject dso, String defaultDescription) {
return dso.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(METADATA_IIIF_DESCRIPTION))
.findFirst().map(m -> m.getValue()).orElse(defaultDescription);
}
public List<String> getIIIFToCs(DSpaceObject dso, String prefix) {
List<String> tocs = dso.getMetadata().stream()
/**
* Return the table of contents (toc) positions in the iiif structure where the
* resource appears. Please note that the same resource can belong to multiple
* ranges (i.e. a page that contains the last paragraph of a section and start
* the new section)
*
* @param bitstream the dspace bitstream
* @param prefix a string to add to all the returned toc inherited from the
* parent dspace object
* @return the iiif tocs for the dspace object
*/
public List<String> getIIIFToCs(Bitstream bitstream, String prefix) {
List<String> tocs = bitstream.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(METADATA_IIIF_TOC))
.map(m -> StringUtils.isNotBlank(prefix) ? prefix + TOC_SEPARATOR + m.getValue() : m.getValue())
.collect(Collectors.toList());
@@ -233,7 +326,13 @@ public class IIIFUtils {
}
}
public String getIIIFFirstToC(Bundle bundle) {
/**
* Return the iiif toc for the specified bundle
*
* @param bundle the dspace bundle
* @return the iiif toc for the specified bundle
*/
public String getBundleIIIFToC(Bundle bundle) {
String label = bundle.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(METADATA_IIIF_LABEL))
.findFirst().map(m -> m.getValue()).orElse(bundle.getName());
@@ -242,40 +341,100 @@ public class IIIFUtils {
.findFirst().map(m -> m.getValue() + TOC_SEPARATOR + label).orElse(label);
}
/**
* Return the iiif viewing hint for the item
*
* @param item the dspace item
* @param defaultHint the default hint to apply if nothing else is defined at
* the item leve
* @return the iiif viewing hint for the item
*/
public String getIIIFViewingHint(Item item, String defaultHint) {
return item.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(METADATA_IIIF_VIEWING_HINT))
.findFirst().map(m -> m.getValue()).orElse(defaultHint);
}
/**
* Return the width for the canvas associated with the bitstream. If the
* bitstream doesn't provide directly the information it is retrieved from the
* bundle, item or default.
*
* @param bitstream the dspace bitstream used in the canvas
* @param bundle the bundle the bitstream belong to
* @param item the item the bitstream belong to
* @param defaultWidth the default width to apply if no other preferences are
* found
* @return the width in pixel for the canvas associated with the bitstream
*/
public int getCanvasWidth(Bitstream bitstream, Bundle bundle, Item item, int defaultWidth) {
return getSizeFromMetadata(bitstream, METADATA_IMAGE_WIDTH,
getSizeFromMetadata(bundle, METADATA_IMAGE_WIDTH,
getSizeFromMetadata(item, METADATA_IMAGE_WIDTH, defaultWidth)));
}
/**
* Return the height for the canvas associated with the bitstream. If the
* bitstream doesn't provide directly the information it is retrieved from the
* bundle, item or default.
*
* @param bitstream the dspace bitstream used in the canvas
* @param bundle the bundle the bitstream belong to
* @param item the item the bitstream belong to
* @param defaultHeight the default width to apply if no other preferences are
* found
* @return the height in pixel for the canvas associated with the bitstream
*/
public int getCanvasHeight(Bitstream bitstream, Bundle bundle, Item item, int defaultHeight) {
return getSizeFromMetadata(bitstream, METADATA_IMAGE_HEIGTH,
getSizeFromMetadata(bundle, METADATA_IMAGE_HEIGTH,
getSizeFromMetadata(item, METADATA_IMAGE_HEIGTH, defaultHeight)));
}
/**
* Utility method to extract an integer from metadata value. The defaultValue is
* returned if there are not values for the specified metadata or the value is
* not a valid integer. Only the first metadata value if any is used
*
* @param dso the dspace object
* @param metadata the metadata key (schema.element[.qualifier]
* @param defaultValue default to return if the metadata value is not an integer
* @return an integer from metadata value
*/
private int getSizeFromMetadata(DSpaceObject dso, String metadata, int defaultValue) {
return dso.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(metadata))
.findFirst().map(m -> castToInt(m, defaultValue)).orElse(defaultValue);
}
private int castToInt(MetadataValue m, int defaultWidth) {
/**
* Utility method to cast a metadata value to int. The defaultInt is returned if
* the metadata value is not a valid integer
*
* @param m the metadata value
* @param defaultIntthe default to return if the metadata value is not an
* integer
* @return an int corresponding to the metadata value
*/
private int castToInt(MetadataValue m, int defaultInt) {
try {
return Integer.parseInt(m.getValue());
} catch (NumberFormatException e) {
log.error("Error parsing " + m.getMetadataField().toString('.') + " of " + m.getDSpaceObject().getID()
+ " the value " + m.getValue() + " is not an integer. Returning the default.");
}
return defaultWidth;
return defaultInt;
}
/**
* Return the prefix to use to generate canvas name for canvas that has no an
* explicit IIIF label
*
* @param item the DSpace Item
* @param defaultNaming a default to return if the item has not a custom value
* @return the prefix to use to generate canvas name for canvas that has no an
* explicit IIIF label
*/
public String getCanvasNaming(Item item, String defaultNaming) {
return item.getMetadata().stream()
.filter(m -> m.getMetadataField().toString('.').contentEquals(METADATA_IIIF_CANVAS_NAMING))

View File

@@ -69,7 +69,7 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
}
@Test
public void findOneIIIFSearchableEntityTypeIT() throws Exception {
public void findOneIIIFSearchableEntityTypeWithGlobalConfigIT() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
@@ -97,7 +97,7 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream2 = BitstreamBuilder
.createBitstream(context, publicItem1, is, IIIFBundle)
.createBitstream(context, publicItem1, is)
.withName("Bitstream2.png")
.withMimeType("image/png")
.build();
@@ -112,23 +112,42 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.andExpect(jsonPath("$.service.profile", is("http://iiif.io/api/search/0/search")))
.andExpect(jsonPath("$.thumbnail.@id", Matchers.containsString("/iiif/2/"
+ bitstream1.getID())))
.andExpect(jsonPath("$.metadata[0].label", is("Title")))
.andExpect(jsonPath("$.metadata[0].value", is("Public item 1")))
.andExpect(jsonPath("$.metadata[1].label", is("Issue Date")))
.andExpect(jsonPath("$.metadata[1].value", is("2017-10-17")))
.andExpect(jsonPath("$.metadata[2].label", is("Authors")))
.andExpect(jsonPath("$.metadata[2].value[0]", is("Smith, Donald")))
.andExpect(jsonPath("$.metadata[2].value[1]", is("Doe, John")))
.andExpect(jsonPath("$.sequences[0].canvases[0].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0")))
.andExpect(jsonPath("$.sequences[0].canvases[0].label", is("Page 1")))
.andExpect(jsonPath("$.sequences[0].canvases[0].width", is(1200)))
.andExpect(jsonPath("$.sequences[0].canvases[0].images[0].resource.service.@id",
Matchers.endsWith(bitstream1.getID().toString())))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[0].label", is("File name")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[0].value", is("Bitstream1.jpg")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[1].label", is("Format")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[1].value", is("JPEG")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[2].label", is("Mime Type")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[2].value", is("image/jpeg")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[3].label", is("File size")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[3].value", is("19 bytes")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[4].label", is("Checksum")))
.andExpect(jsonPath("$.sequences[0].canvases[0].metadata[4].value",
is("11e23c5702595ba512c1c2ee8e8d6153 (MD5)")))
.andExpect(jsonPath("$.sequences[0].canvases[1].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c1")))
.andExpect(jsonPath("$.sequences[0].canvases[1].label", is("Page 2")))
.andExpect(jsonPath("$.sequences[0].canvases[1].images[0].resource.service.@id",
Matchers.endsWith(bitstream2.getID().toString())))
.andExpect(jsonPath("$.structures").doesNotExist())
.andExpect(jsonPath("$.related.@id",
Matchers.containsString("/items/" + publicItem1.getID())));
}
@Test
public void findOneIIIFSearchableWithGlobalConfigIT() throws Exception {
public void findOneIIIFSearchableWithMixedConfigIT() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
@@ -142,19 +161,26 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.enableIIIF()
.withIIIFCanvasWidth(2000)
.withIIIFCanvasHeight(3000)
.withEntityType("IIIFSearchable")
.withIIIFCanvasNaming("Global")
.enableIIIFSearch()
.build();
String bitstreamContent = "ThisIsSomeText";
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder
.createBitstream(context, publicItem1, is, IIIFBundle)
.createBitstream(context, publicItem1, is)
.withName("Bitstream1.jpg")
.withMimeType("image/jpeg")
.withIIIFLabel("Custom Label")
.withIIIFCanvasWidth(3163)
.withIIIFCanvasHeight(4220)
//.withMimeType("image/jpeg")
.build();
}
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream2.jpg")
.withMimeType("image/jpeg")
.build();
}
context.restoreAuthSystemState();
@@ -164,14 +190,20 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.andExpect(jsonPath("$.@context", is("http://iiif.io/api/presentation/2/context.json")))
.andExpect(jsonPath("$.sequences[0].canvases[0].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0")))
.andExpect(jsonPath("$.sequences[0].canvases[0].label", is("Global 1")))
.andExpect(jsonPath("$.sequences[0].canvases[0].width", is(2000)))
.andExpect(jsonPath("$.sequences[0].canvases[0].height", is(3000)))
.andExpect(jsonPath("$.sequences[0].canvases[0].label", is("Custom Label")))
.andExpect(jsonPath("$.sequences[0].canvases[0].width", is(3163)))
.andExpect(jsonPath("$.sequences[0].canvases[0].height", is(4220)))
.andExpect(jsonPath("$.sequences[0].canvases[1].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c1")))
.andExpect(jsonPath("$.sequences[0].canvases[1].label", is("Global 2")))
.andExpect(jsonPath("$.sequences[0].canvases[1].width", is(2000)))
.andExpect(jsonPath("$.sequences[0].canvases[1].height", is(3000)))
.andExpect(jsonPath("$.structures").doesNotExist())
.andExpect(jsonPath("$.service").exists());
}
@Test
public void findOneIIIFSearchableWithInfoJsonIT() throws Exception {
public void findOneIIIFSearchableWithCustomBundleAndConfigIT() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
@@ -208,6 +240,7 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.andExpect(jsonPath("$.sequences[0].canvases[0].label", is("Custom Label")))
.andExpect(jsonPath("$.sequences[0].canvases[0].width", is(3163)))
.andExpect(jsonPath("$.sequences[0].canvases[0].height", is(4220)))
.andExpect(jsonPath("$.structures").doesNotExist())
.andExpect(jsonPath("$.service").exists());
}
@@ -227,6 +260,15 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.enableIIIF()
.build();
Item publicItem2 = ItemBuilder.createItem(context, col1)
.withTitle("Public item 2")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withMetadata("dc", "rights", "uri", "https://license.org")
.withIIIFViewingHint("paged")
.enableIIIF()
.build();
String bitstreamContent = "ThisIsSomeDummyText";
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder.
@@ -234,6 +276,12 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.withName("Bitstream1.jpg")
.withMimeType("image/jpeg")
.build();
BitstreamBuilder.
createBitstream(context, publicItem2, is, IIIFBundle)
.withName("Bitstream1.jpg")
.withMimeType("image/jpeg")
.build();
}
String bitstreamContent2 = "ThisIsSomeDummyText2";
@@ -243,6 +291,12 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.withName("Bitstream2.png")
.withMimeType("image/png")
.build();
BitstreamBuilder.
createBitstream(context, publicItem2, is, IIIFBundle)
.withName("Bitstream1.jpg")
.withMimeType("image/jpeg")
.build();
}
String bitstreamContent3 = "ThisIsSomeDummyText3";
@@ -252,24 +306,32 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.withName("Bitstream3.tiff")
.withMimeType("image/tiff")
.build();
BitstreamBuilder.
createBitstream(context, publicItem2, is, IIIFBundle)
.withName("Bitstream3.tiff")
.withMimeType("image/tiff")
.build();
}
context.restoreAuthSystemState();
// With more than 2 bitstreams in IIIF bundle, the sequence viewing hint should be "paged"
// unless that has been changed in dspace configuration. This test assumes that DSpace
// has been configured to return the "individuals" hint for documents to better support
// search results in Mirador. That is the current dspace.cfg default setting.
// The sequence viewing hint should be "individuals" in the item metadata.
getClient().perform(get("/iiif/" + publicItem1.getID() + "/manifest"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.license", is("https://license.org")))
.andExpect(jsonPath("$.@context", is("http://iiif.io/api/presentation/2/context.json")))
.andExpect(jsonPath("$.sequences[0].canvases[0].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0")))
.andExpect(jsonPath("$.sequences[0].canvases[0].label", is("Page 1")))
.andExpect(jsonPath("$.sequences[0].canvases", Matchers.hasSize(3)))
.andExpect(jsonPath("$.viewingHint", is("individuals")))
.andExpect(jsonPath("$.service").doesNotExist());
getClient().perform(get("/iiif/" + publicItem2.getID() + "/manifest"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.license", is("https://license.org")))
.andExpect(jsonPath("$.@context", is("http://iiif.io/api/presentation/2/context.json")))
.andExpect(jsonPath("$.sequences[0].canvases", Matchers.hasSize(3)))
.andExpect(jsonPath("$.viewingHint", is("paged")))
.andExpect(jsonPath("$.service").doesNotExist());
}
@Test
@@ -353,7 +415,274 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
}
@Test
public void findOneIIIFEntityTypeIT() throws Exception {
public void findOneWithBundleStructures() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1")
.build();
Item publicItem1 = ItemBuilder.createItem(context, col1)
.withTitle("Public item 1")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withIIIFCanvasHeight(3000)
.withIIIFCanvasWidth(2000)
.withIIIFCanvasNaming("Global")
.enableIIIF()
.enableIIIFSearch()
.build();
String bitstreamContent = "ThisIsSomeText";
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream1.jpg")
.withMimeType("image/jpeg")
.build();
}
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder
.createBitstream(context, publicItem1, is, IIIFBundle)
.withName("Bitstream2.png")
.withMimeType("image/png")
.build();
}
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder
.createBitstream(context, publicItem1, is, IIIFBundle)
.withName("Bitstream3.tiff")
.withMimeType("image/tiff")
.build();
}
context.restoreAuthSystemState();
// expect structures elements with label and canvas id.
getClient().perform(get("/iiif/" + publicItem1.getID() + "/manifest"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.@context", is("http://iiif.io/api/presentation/2/context.json")))
.andExpect(jsonPath("$.sequences[0].canvases[0].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0")))
.andExpect(jsonPath("$.sequences[0].canvases[0].label", is("Global 1")))
.andExpect(jsonPath("$.sequences[0].canvases[0].width", is(2000)))
.andExpect(jsonPath("$.sequences[0].canvases[0].height", is(3000)))
.andExpect(jsonPath("$.sequences[0].canvases[1].label", is("Global 2")))
.andExpect(jsonPath("$.sequences[0].canvases[2].label", is("Global 3")))
.andExpect(jsonPath("$.structures[0].@id",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0")))
.andExpect(jsonPath("$.structures[0].label", is("Table of Contents")))
.andExpect(jsonPath("$.structures[0].ranges[0]",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0")))
.andExpect(jsonPath("$.structures[0].ranges[1]",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-1")))
.andExpect(jsonPath("$.structures[1].@id",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0")))
.andExpect(jsonPath("$.structures[1].label", is("ORIGINAL")))
.andExpect(jsonPath("$.structures[1].canvases[0]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0")))
.andExpect(jsonPath("$.structures[2].@id",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-1")))
.andExpect(jsonPath("$.structures[2].label", is("IIIF")))
.andExpect(jsonPath("$.structures[2].canvases[0]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c1")))
.andExpect(jsonPath("$.structures[2].canvases[1]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c2")))
.andExpect(jsonPath("$.service").exists());
}
@Test
public void findOneWithHierarchicalStructures() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1")
.build();
Item publicItem1 = ItemBuilder.createItem(context, col1)
.withTitle("Public item 1")
.withIssueDate("2017-10-17")
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.enableIIIF()
.enableIIIFSearch()
.build();
String bitstreamContent = "ThisIsSomeText";
Bitstream bitstream1 = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream1 = BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream1.jpg")
.withMimeType("image/jpeg")
.withIIIFToC("Section 1")
.build();
}
Bitstream bitstream2 = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream2 = BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream2.jpg")
.withMimeType("image/jpeg")
.withIIIFToC("Section 1|||a")
.build();
}
Bitstream bitstream3 = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream3 = BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream3.jpg")
.withMimeType("image/jpeg")
.withIIIFToC("Section 1|||a")
.build();
}
Bitstream bitstream4 = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream4 = BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream4.jpg")
.withMimeType("image/jpeg")
.withIIIFToC("Section 1|||b")
.build();
}
Bitstream bitstream5 = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream5 = BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream5.jpg")
.withMimeType("image/jpeg")
.withIIIFToC("Section 1")
.build();
}
Bitstream bitstream6 = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream6 = BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream6.png")
.withMimeType("image/png")
.withIIIFToC("Section 2")
.build();
}
Bitstream bitstream7 = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream7 = BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream7.tiff")
.withMimeType("image/tiff")
.withIIIFToC("Section 2")
.build();
}
Bitstream bitstream8 = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
bitstream8 = BitstreamBuilder
.createBitstream(context, publicItem1, is)
.withName("Bitstream8.tiff")
.withMimeType("image/tiff")
.withIIIFToC("Section 2|||sub 2-1")
.build();
}
context.restoreAuthSystemState();
// expect structures elements with label and canvas id.
getClient().perform(get("/iiif/" + publicItem1.getID() + "/manifest"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.@context", is("http://iiif.io/api/presentation/2/context.json")))
.andExpect(jsonPath("$.sequences[0].canvases", Matchers.hasSize(8)))
.andExpect(jsonPath("$.sequences[0].canvases[0].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0")))
.andExpect(jsonPath("$.sequences[0].canvases[0].images[0].resource.@id",
Matchers.containsString(bitstream1.getID().toString())))
.andExpect(jsonPath("$.sequences[0].canvases[1].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c1")))
.andExpect(jsonPath("$.sequences[0].canvases[1].images[0].resource.@id",
Matchers.containsString(bitstream2.getID().toString())))
.andExpect(jsonPath("$.sequences[0].canvases[2].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c2")))
.andExpect(jsonPath("$.sequences[0].canvases[2].images[0].resource.@id",
Matchers.containsString(bitstream3.getID().toString())))
.andExpect(jsonPath("$.sequences[0].canvases[3].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c3")))
.andExpect(jsonPath("$.sequences[0].canvases[3].images[0].resource.@id",
Matchers.containsString(bitstream4.getID().toString())))
.andExpect(jsonPath("$.sequences[0].canvases[4].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c4")))
.andExpect(jsonPath("$.sequences[0].canvases[4].images[0].resource.@id",
Matchers.containsString(bitstream5.getID().toString())))
.andExpect(jsonPath("$.sequences[0].canvases[5].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c5")))
.andExpect(jsonPath("$.sequences[0].canvases[5].images[0].resource.@id",
Matchers.containsString(bitstream6.getID().toString())))
.andExpect(jsonPath("$.sequences[0].canvases[6].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c6")))
.andExpect(jsonPath("$.sequences[0].canvases[6].images[0].resource.@id",
Matchers.containsString(bitstream7.getID().toString())))
.andExpect(jsonPath("$.sequences[0].canvases[7].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c7")))
.andExpect(jsonPath("$.sequences[0].canvases[7].images[0].resource.@id",
Matchers.containsString(bitstream8.getID().toString())))
.andExpect(jsonPath("$.structures[0].@id",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0")))
// the toc contains two top sections 1 & 2 without direct children canvases
.andExpect(jsonPath("$.structures[0].label", is("Table of Contents")))
.andExpect(jsonPath("$.structures[0].ranges", Matchers.hasSize(2)))
.andExpect(jsonPath("$.structures[0].ranges[0]",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0")))
.andExpect(jsonPath("$.structures[0].ranges[1]",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-1")))
.andExpect(jsonPath("$.structures[0].canvases").doesNotExist())
// section 1 contains bitstream 1 and 5 and the sub section a and b
.andExpect(jsonPath("$.structures[1].@id",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0")))
.andExpect(jsonPath("$.structures[1].label", is("Section 1")))
.andExpect(jsonPath("$.structures[1].ranges", Matchers.hasSize(2)))
.andExpect(jsonPath("$.structures[1].ranges[0]",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0-0")))
.andExpect(jsonPath("$.structures[1].ranges[1]",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0-1")))
.andExpect(jsonPath("$.structures[1].canvases", Matchers.hasSize(2)))
.andExpect(jsonPath("$.structures[1].canvases[0]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0")))
.andExpect(jsonPath("$.structures[1].canvases[1]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c4")))
// section 1 > a contains bitstream 2 and 3, no sub sections
.andExpect(jsonPath("$.structures[2].label", is("a")))
.andExpect(jsonPath("$.structures[2].ranges").doesNotExist())
.andExpect(jsonPath("$.structures[2].canvases", Matchers.hasSize(2)))
.andExpect(jsonPath("$.structures[2].canvases[0]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c1")))
.andExpect(jsonPath("$.structures[2].canvases[1]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c2")))
// section 1 > b contains only the bitstream 4 and no sub sections
.andExpect(jsonPath("$.structures[3].@id",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-0-1")))
.andExpect(jsonPath("$.structures[3].label", is("b")))
.andExpect(jsonPath("$.structures[3].ranges").doesNotExist())
.andExpect(jsonPath("$.structures[3].canvases", Matchers.hasSize(1)))
.andExpect(jsonPath("$.structures[3].canvases[0]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c3")))
// section 2 contains bitstream 6 and 7, sub section "sub 2-1"
.andExpect(jsonPath("$.structures[4].label", is("Section 2")))
.andExpect(jsonPath("$.structures[4].ranges", Matchers.hasSize(1)))
.andExpect(jsonPath("$.structures[4].ranges[0]",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-1-0")))
.andExpect(jsonPath("$.structures[4].canvases", Matchers.hasSize(2)))
.andExpect(jsonPath("$.structures[4].canvases[0]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c5")))
.andExpect(jsonPath("$.structures[4].canvases[1]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c6")))
// section 2 > sub 2-1 contains only the bitstream 8 no sub sections
.andExpect(jsonPath("$.structures[5].@id",
Matchers.endsWith("/iiif/" + publicItem1.getID() + "/manifest/range/r0-1-0")))
.andExpect(jsonPath("$.structures[5].label", is("sub 2-1")))
.andExpect(jsonPath("$.structures[5].ranges").doesNotExist())
.andExpect(jsonPath("$.structures[5].canvases", Matchers.hasSize(1)))
.andExpect(jsonPath("$.structures[5].canvases[0]",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c7")))
.andExpect(jsonPath("$.service").exists());
}
@Test
public void findOneIIIFNotSearcheableIT() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
@@ -463,14 +792,22 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
BitstreamBuilder.
createBitstream(context, publicItem1, is)
.withName("Bitstream2.pdf")
.withName("Bitstream2.mp4")
.withMimeType("video/mp4")
.build();
}
Bitstream pdf = null;
try (InputStream is = IOUtils.toInputStream(bitstreamContent, CharEncoding.UTF_8)) {
pdf = BitstreamBuilder.
createBitstream(context, publicItem1, is)
.withName("Bitstream3.pdf")
.withMimeType("application/pdf")
.build();
}
context.restoreAuthSystemState();
// Image in the ORIGINAL bundle added as canvas; PDF ignored...
// Image in the ORIGINAL bundle added as canvas; MP4 ignored, PDF offered as rendering...
getClient().perform(get("/iiif/" + publicItem1.getID() + "/manifest"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.@context", is("http://iiif.io/api/presentation/2/context.json")))
@@ -478,6 +815,10 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
.andExpect(jsonPath("$.sequences[0].canvases[0].@id",
Matchers.containsString("/iiif/" + publicItem1.getID() + "/canvas/c0")))
.andExpect(jsonPath("$.sequences[0].canvases[0].label", is("Page 1")))
.andExpect(jsonPath("$.sequences[0].rendering.@id",
Matchers.endsWith(pdf.getID().toString() + "/content")))
.andExpect(jsonPath("$.sequences[0].rendering.label", is("Bitstream3.pdf")))
.andExpect(jsonPath("$.sequences[0].rendering.format", is("application/pdf")))
.andExpect(jsonPath("$.service").doesNotExist());
}
@@ -511,6 +852,16 @@ public class IIIFControllerIT extends AbstractControllerIntegrationTest {
getClient().perform(get("/iiif/" + publicItem1.getID() + "/canvas/c0"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.@type", is("sc:Canvas")))
.andExpect(jsonPath("$.metadata[0].label", is("File name")))
.andExpect(jsonPath("$.metadata[0].value", is("IMG1.jpg")))
.andExpect(jsonPath("$.metadata[1].label", is("Format")))
.andExpect(jsonPath("$.metadata[1].value", is("JPEG")))
.andExpect(jsonPath("$.metadata[2].label", is("Mime Type")))
.andExpect(jsonPath("$.metadata[2].value", is("image/jpeg")))
.andExpect(jsonPath("$.metadata[3].label", is("File size")))
.andExpect(jsonPath("$.metadata[3].value", is("19 bytes")))
.andExpect(jsonPath("$.metadata[4].label", is("Checksum")))
.andExpect(jsonPath("$.metadata[4].value", is("11e23c5702595ba512c1c2ee8e8d6153 (MD5)")))
.andExpect(jsonPath("$.images[0].@type", is("oa:Annotation")));
}

View File

@@ -44,16 +44,17 @@ iiif.attribution = ${dspace.name}
iiif.logo.image = ${dspace.ui.url}/assets/images/dspace-logo.svg
# (optional) one of individuals, paged or continuous. Can be overridden at the item level via
# the iiif.view.hint metadata
iiif.document.viewing.hint =
iiif.document.viewing.hint = individuals
# ????
iiif.url =
# public base url of a iiif server able to serve DSpace images. The bitstream uuid is appended to this URL
iiif.image.server =
# these should be not changed. They must match the routing configuration for the iiif controller
iiif.url = ${dspace.server.url}/iiif
iiif.solr.search.url =
# ????
iiif.bitstream.url =
iiif.bitstream.url = ${dspace.server.url}/api/core/bitstreams
# default value for the canvas size. Can be overridden at the item, bundle or bitstream level
# via the iiif.image.width e iiif.image.height metadata