package edu.illinois.library.cantaloupe.resource.iiif.v2;
import edu.illinois.library.cantaloupe.RestletApplication;
import edu.illinois.library.cantaloupe.cache.CacheFacade;
import edu.illinois.library.cantaloupe.config.Configuration;
import edu.illinois.library.cantaloupe.config.Key;
import edu.illinois.library.cantaloupe.image.Format;
import edu.illinois.library.cantaloupe.image.Identifier;
import edu.illinois.library.cantaloupe.image.Info;
import edu.illinois.library.cantaloupe.image.MediaType;
import edu.illinois.library.cantaloupe.operation.OperationList;
import edu.illinois.library.cantaloupe.processor.Processor;
import edu.illinois.library.cantaloupe.processor.ProcessorFactory;
import edu.illinois.library.cantaloupe.processor.UnsupportedOutputFormatException;
import edu.illinois.library.cantaloupe.processor.UnsupportedSourceFormatException;
import edu.illinois.library.cantaloupe.source.Source;
import edu.illinois.library.cantaloupe.source.SourceFactory;
import edu.illinois.library.cantaloupe.processor.ProcessorConnector;
import edu.illinois.library.cantaloupe.resource.CachedImageRepresentation;
import edu.illinois.library.cantaloupe.resource.IllegalClientArgumentException;
import edu.illinois.library.cantaloupe.resource.ImageRepresentation;
import edu.illinois.library.cantaloupe.resource.iiif.SizeRestrictedException;
import org.restlet.data.Disposition;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.resource.Get;
import java.awt.Dimension;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.greenstone.gsdl3.IIIFServerBridge;
/**
* Handles IIIF Image API 2.x image requests.
*
* @see Image
* Request Operations
*/
public class GSImageResource extends IIIF2Resource {
protected Source createSource(Identifier identifier) throws Exception
{
String identifier_str = identifier.toString();
String[] strs = identifier_str.split(":", 3);
if(strs == null || strs.length < 3) {
System.err.println("identifier is not in the form site:coll:id" + identifier_str);
return null;
}
String site_name = strs[0];
String coll_name = strs[1];
String doc_id = strs[2];
// Move into Constructor ???
IIIFServerBridge gs_iiif_bridge = new IIIFServerBridge();
// and keep cache of of bridges in hashmap, keyed on sitename??
gs_iiif_bridge.init(site_name);
String collect_image_filename = gs_iiif_bridge.doGetDocumentMessage(coll_name + ":" + doc_id);
String site_image_filename = site_name + "/collect/" + coll_name + "/index/assoc/" + collect_image_filename;
System.err.println("**** Greenstone site image filename = " + site_image_filename);
final Identifier identifier_image = new Identifier(site_image_filename);
System.err.println("***** identifier_image = " + identifier_image);
final Source source = new SourceFactory().newSource(identifier_image, getDelegateProxy());
System.err.println("***** source path = " + ((edu.illinois.library.cantaloupe.source.FileSource)source).getPath());
return source;
}
/**
* Responds to image requests.
*/
@Get
public Representation doGet() throws Exception {
final Configuration config = Configuration.getInstance();
final Map attrs = getRequest().getAttributes();
final Identifier identifier = getIdentifier();
final CacheFacade cacheFacade = new CacheFacade();
// Assemble the URI parameters into a Parameters object.
final Parameters params = new Parameters(
identifier,
(String) attrs.get("region"),
(String) attrs.get("size"),
(String) attrs.get("rotation"),
(String) attrs.get("quality"),
(String) attrs.get("format"));
final OperationList ops = params.toOperationList();
ops.getOptions().putAll(
getReference().getQueryAsForm(true).getValuesMap());
final Disposition disposition = getRepresentationDisposition(
getReference().getQueryAsForm()
.getFirstValue(RESPONSE_CONTENT_DISPOSITION_QUERY_ARG),
ops.getIdentifier(), ops.getOutputFormat());
Format sourceFormat = Format.UNKNOWN;
// If we don't need to resolve first, and are using a cache:
// 1. If the cache contains an image matching the request, skip all the
// setup and just return the cached image.
// 2. Otherwise, if the cache contains a relevant info, get it to avoid
// having to get it from a source later.
if (!isResolvingFirst()) {
final Info info = cacheFacade.getInfo(identifier);
if (info != null) {
ops.applyNonEndpointMutations(info, getDelegateProxy());
InputStream cacheStream = null;
try {
cacheStream = cacheFacade.newDerivativeImageInputStream(ops);
} catch (IOException e) {
// Don't rethrow -- it's still possible to service the
// request.
getLogger().severe(e.getMessage());
}
if (cacheStream != null) {
addLinkHeader(params);
commitCustomResponseHeaders();
return new CachedImageRepresentation(
cacheStream,
params.getOutputFormat().toFormat().getPreferredMediaType(),
disposition);
} else {
Format infoFormat = info.getSourceFormat();
if (infoFormat != null) {
sourceFormat = infoFormat;
}
}
}
}
final Source source = createSource(identifier);
//final Source source = new SourceFactory().newSource(
// identifier, getDelegateProxy());
// If we are resolving first, or if the source image is not present in
// the source cache (if enabled), check access to it in preparation for
// retrieval.
final Path sourceImage = cacheFacade.getSourceCacheFile(identifier);
if (sourceImage == null || isResolvingFirst()) {
try {
source.checkAccess();
} catch (NoSuchFileException e) { // this needs to be rethrown!
if (config.getBoolean(Key.CACHE_SERVER_PURGE_MISSING, false)) {
// If the image was not found, purge it from the cache.
cacheFacade.purgeAsync(ops.getIdentifier());
}
throw e;
}
}
// If we don't know the format yet, get it.
if (Format.UNKNOWN.equals(sourceFormat)) {
// If we are not resolving first, and there is a hit in the source
// cache, read the format from the source-cached-file, as we will
// expect source cache access to be more efficient.
// Otherwise, read it from the source.
if (!isResolvingFirst() && sourceImage != null) {
List mediaTypes = MediaType.detectMediaTypes(sourceImage);
if (!mediaTypes.isEmpty()) {
sourceFormat = mediaTypes.get(0).toFormat();
}
} else {
sourceFormat = source.getFormat();
}
}
// Obtain an instance of the processor assigned to that format. This
// must eventually be close()d, but we don't want to close it here
// unless there is an error.
final Processor processor = new ProcessorFactory().
newProcessor(sourceFormat);
try {
// Connect it to the source.
tempFileFuture = new ProcessorConnector().connect(
source, processor, identifier, sourceFormat);
final Info info = getOrReadInfo(ops.getIdentifier(), processor);
Dimension fullSize;
try {
fullSize = info.getSize(getPageIndex());
} catch (IndexOutOfBoundsException e) {
throw new IllegalClientArgumentException(e);
}
getRequestContext().setOperationList(ops, fullSize);
StringRepresentation redirectingRep = checkRedirect();
if (redirectingRep != null) {
return redirectingRep;
}
checkAuthorization();
validateRequestedArea(ops, sourceFormat, fullSize);
try {
processor.validate(ops, fullSize);
} catch (IllegalArgumentException e) {
throw new IllegalClientArgumentException(e.getMessage(), e);
}
final Dimension resultingSize = ops.getResultingSize(info.getSize());
validateSize(resultingSize, info.getOrientationSize(), processor);
try {
ops.applyNonEndpointMutations(info, getDelegateProxy());
} catch (IllegalStateException e) {
// applyNonEndpointMutations() will freeze the instance, and it
// may have already been called. That's fine.
}
// Find out whether the processor supports the source format by asking
// it whether it offers any output formats for it.
Set availableOutputFormats = processor.getAvailableOutputFormats();
if (!availableOutputFormats.isEmpty()) {
if (!availableOutputFormats.contains(ops.getOutputFormat())) {
Exception e = new UnsupportedOutputFormatException(
processor, ops.getOutputFormat());
getLogger().warning(e.getMessage() + ": " + getReference());
throw e;
}
} else {
throw new UnsupportedSourceFormatException(sourceFormat);
}
addLinkHeader(params);
commitCustomResponseHeaders();
return new ImageRepresentation(info, processor, ops, disposition,
isBypassingCache(), () -> {
if (tempFileFuture != null) {
Path tempFile = tempFileFuture.get();
if (tempFile != null) {
Files.deleteIfExists(tempFile);
}
}
return null;
});
} catch (Throwable t) {
processor.close();
throw t;
}
}
private void addLinkHeader(Parameters params) {
final Identifier identifier = params.getIdentifier();
final String paramsStr = params.toString().replaceFirst(
identifier.toString(), getPublicIdentifier());
getBufferedResponseHeaders().add("Link",
String.format("<%s%s/%s>;rel=\"canonical\"",
getPublicRootReference(),
RestletApplication.IIIF_2_PATH, paramsStr));
}
private void validateSize(Dimension resultingSize,
Dimension virtualSize,
Processor processor) {
final Configuration config = Configuration.getInstance();
if (config.getBoolean(Key.IIIF_2_RESTRICT_TO_SIZES, false)) {
final ImageInfoFactory factory = new ImageInfoFactory(
processor.getSupportedFeatures(),
processor.getSupportedIIIF2Qualities(),
processor.getAvailableOutputFormats());
final List sizes = factory.getSizes(virtualSize);
boolean ok = false;
for (ImageInfo.Size size : sizes) {
if (size.width == resultingSize.width &&
size.height == resultingSize.height) {
ok = true;
break;
}
}
if (!ok) {
throw new SizeRestrictedException();
}
}
}
private boolean isResolvingFirst() {
return Configuration.getInstance().
getBoolean(Key.CACHE_SERVER_RESOLVE_FIRST, true);
}
}