[32877] | 1 | package edu.illinois.library.cantaloupe.resource.iiif.v2;
|
---|
| 2 |
|
---|
| 3 | import edu.illinois.library.cantaloupe.RestletApplication;
|
---|
| 4 | import edu.illinois.library.cantaloupe.cache.CacheFacade;
|
---|
| 5 | import edu.illinois.library.cantaloupe.config.Configuration;
|
---|
| 6 | import edu.illinois.library.cantaloupe.config.Key;
|
---|
| 7 | import edu.illinois.library.cantaloupe.image.Format;
|
---|
| 8 | import edu.illinois.library.cantaloupe.image.Identifier;
|
---|
| 9 | import edu.illinois.library.cantaloupe.image.Info;
|
---|
| 10 | import edu.illinois.library.cantaloupe.image.MediaType;
|
---|
| 11 | import edu.illinois.library.cantaloupe.operation.OperationList;
|
---|
| 12 | import edu.illinois.library.cantaloupe.processor.Processor;
|
---|
| 13 | import edu.illinois.library.cantaloupe.processor.ProcessorFactory;
|
---|
| 14 | import edu.illinois.library.cantaloupe.processor.UnsupportedOutputFormatException;
|
---|
| 15 | import edu.illinois.library.cantaloupe.processor.UnsupportedSourceFormatException;
|
---|
| 16 | import edu.illinois.library.cantaloupe.source.Source;
|
---|
| 17 | import edu.illinois.library.cantaloupe.source.SourceFactory;
|
---|
| 18 | import edu.illinois.library.cantaloupe.processor.ProcessorConnector;
|
---|
| 19 | import edu.illinois.library.cantaloupe.resource.CachedImageRepresentation;
|
---|
| 20 | import edu.illinois.library.cantaloupe.resource.IllegalClientArgumentException;
|
---|
| 21 | import edu.illinois.library.cantaloupe.resource.ImageRepresentation;
|
---|
| 22 | import edu.illinois.library.cantaloupe.resource.iiif.SizeRestrictedException;
|
---|
| 23 | import org.restlet.data.Disposition;
|
---|
| 24 | import org.restlet.representation.Representation;
|
---|
| 25 | import org.restlet.representation.StringRepresentation;
|
---|
| 26 | import org.restlet.resource.Get;
|
---|
| 27 |
|
---|
| 28 | import java.awt.Dimension;
|
---|
| 29 | import java.io.IOException;
|
---|
| 30 | import java.io.InputStream;
|
---|
| 31 | import java.nio.file.Files;
|
---|
| 32 | import java.nio.file.NoSuchFileException;
|
---|
| 33 | import java.nio.file.Path;
|
---|
| 34 | import java.util.List;
|
---|
| 35 | import java.util.Map;
|
---|
| 36 | import java.util.Set;
|
---|
| 37 |
|
---|
| 38 | import org.greenstone.gsdl3.IIIFServerBridge;
|
---|
| 39 |
|
---|
| 40 | /**
|
---|
| 41 | * Handles IIIF Image API 2.x image requests.
|
---|
| 42 | *
|
---|
| 43 | * @see <a href="http://iiif.io/api/image/2.0/#image-request-parameters">Image
|
---|
| 44 | * Request Operations</a>
|
---|
| 45 | */
|
---|
| 46 | public class GSImageResource extends IIIF2Resource {
|
---|
| 47 |
|
---|
| 48 | protected Source createSource(Identifier identifier) throws Exception
|
---|
| 49 | {
|
---|
| 50 | String identifier_str = identifier.toString();
|
---|
| 51 | String[] strs = identifier_str.split(":", 3);
|
---|
| 52 | if(strs == null || strs.length < 3) {
|
---|
| 53 | System.err.println("identifier is not in the form site:coll:id" + identifier_str);
|
---|
| 54 | return null;
|
---|
| 55 | }
|
---|
| 56 | String site_name = strs[0];
|
---|
| 57 | String coll_name = strs[1];
|
---|
| 58 | String doc_id = strs[2];
|
---|
| 59 |
|
---|
| 60 | // Move into Constructor ???
|
---|
| 61 | IIIFServerBridge gs_iiif_bridge = new IIIFServerBridge();
|
---|
| 62 | // and keep cache of of bridges in hashmap, keyed on sitename??
|
---|
| 63 | gs_iiif_bridge.init(site_name);
|
---|
| 64 |
|
---|
| 65 | String collect_image_filename = gs_iiif_bridge.doGetDocumentMessage(coll_name + ":" + doc_id);
|
---|
| 66 | String site_image_filename = site_name + "/collect/" + coll_name + "/index/assoc/" + collect_image_filename;
|
---|
| 67 |
|
---|
| 68 | System.err.println("**** Greenstone site image filename = " + site_image_filename);
|
---|
| 69 |
|
---|
| 70 | final Identifier identifier_image = new Identifier(site_image_filename);
|
---|
| 71 | System.err.println("***** identifier_image = " + identifier_image);
|
---|
| 72 |
|
---|
| 73 | final Source source = new SourceFactory().newSource(identifier_image, getDelegateProxy());
|
---|
| 74 |
|
---|
| 75 | System.err.println("***** source path = " + ((edu.illinois.library.cantaloupe.source.FileSource)source).getPath());
|
---|
| 76 |
|
---|
| 77 | return source;
|
---|
| 78 | }
|
---|
| 79 |
|
---|
| 80 | /**
|
---|
| 81 | * Responds to image requests.
|
---|
| 82 | */
|
---|
| 83 | @Get
|
---|
| 84 | public Representation doGet() throws Exception {
|
---|
| 85 | final Configuration config = Configuration.getInstance();
|
---|
| 86 | final Map<String,Object> attrs = getRequest().getAttributes();
|
---|
| 87 | final Identifier identifier = getIdentifier();
|
---|
| 88 | final CacheFacade cacheFacade = new CacheFacade();
|
---|
| 89 |
|
---|
| 90 | // Assemble the URI parameters into a Parameters object.
|
---|
| 91 | final Parameters params = new Parameters(
|
---|
| 92 | identifier,
|
---|
| 93 | (String) attrs.get("region"),
|
---|
| 94 | (String) attrs.get("size"),
|
---|
| 95 | (String) attrs.get("rotation"),
|
---|
| 96 | (String) attrs.get("quality"),
|
---|
| 97 | (String) attrs.get("format"));
|
---|
| 98 | final OperationList ops = params.toOperationList();
|
---|
| 99 | ops.getOptions().putAll(
|
---|
| 100 | getReference().getQueryAsForm(true).getValuesMap());
|
---|
| 101 |
|
---|
| 102 | final Disposition disposition = getRepresentationDisposition(
|
---|
| 103 | getReference().getQueryAsForm()
|
---|
| 104 | .getFirstValue(RESPONSE_CONTENT_DISPOSITION_QUERY_ARG),
|
---|
| 105 | ops.getIdentifier(), ops.getOutputFormat());
|
---|
| 106 |
|
---|
| 107 | Format sourceFormat = Format.UNKNOWN;
|
---|
| 108 |
|
---|
| 109 | // If we don't need to resolve first, and are using a cache:
|
---|
| 110 | // 1. If the cache contains an image matching the request, skip all the
|
---|
| 111 | // setup and just return the cached image.
|
---|
| 112 | // 2. Otherwise, if the cache contains a relevant info, get it to avoid
|
---|
| 113 | // having to get it from a source later.
|
---|
| 114 | if (!isResolvingFirst()) {
|
---|
| 115 | final Info info = cacheFacade.getInfo(identifier);
|
---|
| 116 | if (info != null) {
|
---|
| 117 | ops.applyNonEndpointMutations(info, getDelegateProxy());
|
---|
| 118 |
|
---|
| 119 | InputStream cacheStream = null;
|
---|
| 120 | try {
|
---|
| 121 | cacheStream = cacheFacade.newDerivativeImageInputStream(ops);
|
---|
| 122 | } catch (IOException e) {
|
---|
| 123 | // Don't rethrow -- it's still possible to service the
|
---|
| 124 | // request.
|
---|
| 125 | getLogger().severe(e.getMessage());
|
---|
| 126 | }
|
---|
| 127 |
|
---|
| 128 | if (cacheStream != null) {
|
---|
| 129 | addLinkHeader(params);
|
---|
| 130 | commitCustomResponseHeaders();
|
---|
| 131 |
|
---|
| 132 | return new CachedImageRepresentation(
|
---|
| 133 | cacheStream,
|
---|
| 134 | params.getOutputFormat().toFormat().getPreferredMediaType(),
|
---|
| 135 | disposition);
|
---|
| 136 | } else {
|
---|
| 137 | Format infoFormat = info.getSourceFormat();
|
---|
| 138 | if (infoFormat != null) {
|
---|
| 139 | sourceFormat = infoFormat;
|
---|
| 140 | }
|
---|
| 141 | }
|
---|
| 142 | }
|
---|
| 143 | }
|
---|
| 144 |
|
---|
| 145 | final Source source = createSource(identifier);
|
---|
| 146 | //final Source source = new SourceFactory().newSource(
|
---|
| 147 | // identifier, getDelegateProxy());
|
---|
| 148 |
|
---|
| 149 | // If we are resolving first, or if the source image is not present in
|
---|
| 150 | // the source cache (if enabled), check access to it in preparation for
|
---|
| 151 | // retrieval.
|
---|
| 152 | final Path sourceImage = cacheFacade.getSourceCacheFile(identifier);
|
---|
| 153 | if (sourceImage == null || isResolvingFirst()) {
|
---|
| 154 | try {
|
---|
| 155 | source.checkAccess();
|
---|
| 156 | } catch (NoSuchFileException e) { // this needs to be rethrown!
|
---|
| 157 | if (config.getBoolean(Key.CACHE_SERVER_PURGE_MISSING, false)) {
|
---|
| 158 | // If the image was not found, purge it from the cache.
|
---|
| 159 | cacheFacade.purgeAsync(ops.getIdentifier());
|
---|
| 160 | }
|
---|
| 161 | throw e;
|
---|
| 162 | }
|
---|
| 163 | }
|
---|
| 164 |
|
---|
| 165 | // If we don't know the format yet, get it.
|
---|
| 166 | if (Format.UNKNOWN.equals(sourceFormat)) {
|
---|
| 167 | // If we are not resolving first, and there is a hit in the source
|
---|
| 168 | // cache, read the format from the source-cached-file, as we will
|
---|
| 169 | // expect source cache access to be more efficient.
|
---|
| 170 | // Otherwise, read it from the source.
|
---|
| 171 | if (!isResolvingFirst() && sourceImage != null) {
|
---|
| 172 | List<MediaType> mediaTypes = MediaType.detectMediaTypes(sourceImage);
|
---|
| 173 | if (!mediaTypes.isEmpty()) {
|
---|
| 174 | sourceFormat = mediaTypes.get(0).toFormat();
|
---|
| 175 | }
|
---|
| 176 | } else {
|
---|
| 177 | sourceFormat = source.getFormat();
|
---|
| 178 | }
|
---|
| 179 | }
|
---|
| 180 |
|
---|
| 181 | // Obtain an instance of the processor assigned to that format. This
|
---|
| 182 | // must eventually be close()d, but we don't want to close it here
|
---|
| 183 | // unless there is an error.
|
---|
| 184 | final Processor processor = new ProcessorFactory().
|
---|
| 185 | newProcessor(sourceFormat);
|
---|
| 186 |
|
---|
| 187 | try {
|
---|
| 188 | // Connect it to the source.
|
---|
| 189 | tempFileFuture = new ProcessorConnector().connect(
|
---|
| 190 | source, processor, identifier, sourceFormat);
|
---|
| 191 |
|
---|
| 192 | final Info info = getOrReadInfo(ops.getIdentifier(), processor);
|
---|
| 193 | Dimension fullSize;
|
---|
| 194 | try {
|
---|
| 195 | fullSize = info.getSize(getPageIndex());
|
---|
| 196 | } catch (IndexOutOfBoundsException e) {
|
---|
| 197 | throw new IllegalClientArgumentException(e);
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 | getRequestContext().setOperationList(ops, fullSize);
|
---|
| 201 |
|
---|
| 202 | StringRepresentation redirectingRep = checkRedirect();
|
---|
| 203 | if (redirectingRep != null) {
|
---|
| 204 | return redirectingRep;
|
---|
| 205 | }
|
---|
| 206 |
|
---|
| 207 | checkAuthorization();
|
---|
| 208 |
|
---|
| 209 | validateRequestedArea(ops, sourceFormat, fullSize);
|
---|
| 210 |
|
---|
| 211 | try {
|
---|
| 212 | processor.validate(ops, fullSize);
|
---|
| 213 | } catch (IllegalArgumentException e) {
|
---|
| 214 | throw new IllegalClientArgumentException(e.getMessage(), e);
|
---|
| 215 | }
|
---|
| 216 |
|
---|
| 217 | final Dimension resultingSize = ops.getResultingSize(info.getSize());
|
---|
| 218 | validateSize(resultingSize, info.getOrientationSize(), processor);
|
---|
| 219 |
|
---|
| 220 | try {
|
---|
| 221 | ops.applyNonEndpointMutations(info, getDelegateProxy());
|
---|
| 222 | } catch (IllegalStateException e) {
|
---|
| 223 | // applyNonEndpointMutations() will freeze the instance, and it
|
---|
| 224 | // may have already been called. That's fine.
|
---|
| 225 | }
|
---|
| 226 |
|
---|
| 227 | // Find out whether the processor supports the source format by asking
|
---|
| 228 | // it whether it offers any output formats for it.
|
---|
| 229 | Set<Format> availableOutputFormats = processor.getAvailableOutputFormats();
|
---|
| 230 | if (!availableOutputFormats.isEmpty()) {
|
---|
| 231 | if (!availableOutputFormats.contains(ops.getOutputFormat())) {
|
---|
| 232 | Exception e = new UnsupportedOutputFormatException(
|
---|
| 233 | processor, ops.getOutputFormat());
|
---|
| 234 | getLogger().warning(e.getMessage() + ": " + getReference());
|
---|
| 235 | throw e;
|
---|
| 236 | }
|
---|
| 237 | } else {
|
---|
| 238 | throw new UnsupportedSourceFormatException(sourceFormat);
|
---|
| 239 | }
|
---|
| 240 |
|
---|
| 241 | addLinkHeader(params);
|
---|
| 242 | commitCustomResponseHeaders();
|
---|
| 243 | return new ImageRepresentation(info, processor, ops, disposition,
|
---|
| 244 | isBypassingCache(), () -> {
|
---|
| 245 | if (tempFileFuture != null) {
|
---|
| 246 | Path tempFile = tempFileFuture.get();
|
---|
| 247 | if (tempFile != null) {
|
---|
| 248 | Files.deleteIfExists(tempFile);
|
---|
| 249 | }
|
---|
| 250 | }
|
---|
| 251 | return null;
|
---|
| 252 | });
|
---|
| 253 | } catch (Throwable t) {
|
---|
| 254 | processor.close();
|
---|
| 255 | throw t;
|
---|
| 256 | }
|
---|
| 257 | }
|
---|
| 258 |
|
---|
| 259 | private void addLinkHeader(Parameters params) {
|
---|
| 260 | final Identifier identifier = params.getIdentifier();
|
---|
| 261 | final String paramsStr = params.toString().replaceFirst(
|
---|
| 262 | identifier.toString(), getPublicIdentifier());
|
---|
| 263 |
|
---|
| 264 | getBufferedResponseHeaders().add("Link",
|
---|
| 265 | String.format("<%s%s/%s>;rel=\"canonical\"",
|
---|
| 266 | getPublicRootReference(),
|
---|
| 267 | RestletApplication.IIIF_2_PATH, paramsStr));
|
---|
| 268 | }
|
---|
| 269 |
|
---|
| 270 | private void validateSize(Dimension resultingSize,
|
---|
| 271 | Dimension virtualSize,
|
---|
| 272 | Processor processor) {
|
---|
| 273 | final Configuration config = Configuration.getInstance();
|
---|
| 274 |
|
---|
| 275 | if (config.getBoolean(Key.IIIF_2_RESTRICT_TO_SIZES, false)) {
|
---|
| 276 | final ImageInfoFactory factory = new ImageInfoFactory(
|
---|
| 277 | processor.getSupportedFeatures(),
|
---|
| 278 | processor.getSupportedIIIF2Qualities(),
|
---|
| 279 | processor.getAvailableOutputFormats());
|
---|
| 280 |
|
---|
| 281 | final List<ImageInfo.Size> sizes = factory.getSizes(virtualSize);
|
---|
| 282 |
|
---|
| 283 | boolean ok = false;
|
---|
| 284 | for (ImageInfo.Size size : sizes) {
|
---|
| 285 | if (size.width == resultingSize.width &&
|
---|
| 286 | size.height == resultingSize.height) {
|
---|
| 287 | ok = true;
|
---|
| 288 | break;
|
---|
| 289 | }
|
---|
| 290 | }
|
---|
| 291 | if (!ok) {
|
---|
| 292 | throw new SizeRestrictedException();
|
---|
| 293 | }
|
---|
| 294 | }
|
---|
| 295 | }
|
---|
| 296 |
|
---|
| 297 | private boolean isResolvingFirst() {
|
---|
| 298 | return Configuration.getInstance().
|
---|
| 299 | getBoolean(Key.CACHE_SERVER_RESOLVE_FIRST, true);
|
---|
| 300 | }
|
---|
| 301 |
|
---|
| 302 | }
|
---|