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