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 | }
|
---|