1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.mycore.common.content.util;
19
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.OutputStream;
23 import java.text.MessageFormat;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.StringTokenizer;
27
28 import org.apache.logging.log4j.LogManager;
29 import org.apache.logging.log4j.Logger;
30 import org.mycore.common.content.MCRContent;
31
32 import jakarta.servlet.ServletConfig;
33 import jakarta.servlet.ServletContext;
34 import jakarta.servlet.ServletOutputStream;
35 import jakarta.servlet.ServletResponseWrapper;
36 import jakarta.servlet.http.HttpServletRequest;
37 import jakarta.servlet.http.HttpServletResponse;
38
39
40
41
42
43 public abstract class MCRServletContentHelper {
44
45 public static final int DEFAULT_BUFFER_SIZE = ContentUtils.DEFAULT_BUFFER_SIZE;
46
47 public static final String ATT_SERVE_CONTENT = MCRServletContentHelper.class.getName() + ".serveContent";
48
49 private static Logger LOGGER = LogManager.getLogger(MCRServletContentHelper.class);
50
51 public static Config buildConfig(ServletConfig servletConfig) {
52 Config config = new Config();
53
54 if (servletConfig.getInitParameter("inputBufferSize") != null) {
55 config.inputBufferSize = Integer.parseInt(servletConfig.getInitParameter("inputBufferSize"));
56 }
57
58 if (servletConfig.getInitParameter("outputBufferSize") != null) {
59 config.outputBufferSize = Integer.parseInt(servletConfig.getInitParameter("outputBufferSize"));
60 }
61
62 if (servletConfig.getInitParameter("useAcceptRanges") != null) {
63 config.useAcceptRanges = Boolean.parseBoolean(servletConfig.getInitParameter("useAcceptRanges"));
64 }
65
66 if (config.inputBufferSize < ContentUtils.MIN_BUFFER_SIZE) {
67 config.inputBufferSize = ContentUtils.MIN_BUFFER_SIZE;
68 }
69 if (config.outputBufferSize < ContentUtils.MIN_BUFFER_SIZE) {
70 config.outputBufferSize = ContentUtils.MIN_BUFFER_SIZE;
71 }
72 return config;
73 }
74
75 public static boolean isServeContent(final HttpServletRequest request) {
76 return request.getAttribute(ATT_SERVE_CONTENT) != Boolean.FALSE;
77 }
78
79
80
81
82
83 public static void serveContent(final MCRContent content, final HttpServletRequest request,
84 final HttpServletResponse response, final ServletContext context) throws IOException {
85 serveContent(content, request, response, context, new Config(), isServeContent(request));
86 }
87
88
89
90
91
92 public static void serveContent(final MCRContent content, final HttpServletRequest request,
93 final HttpServletResponse response, final ServletContext context, final Config config,
94 final boolean withContent)
95 throws IOException {
96
97 boolean serveContent = withContent;
98
99 final String path = getRequestPath(request);
100 if (LOGGER.isDebugEnabled()) {
101 if (serveContent) {
102 LOGGER.debug("Serving '{}' headers and data", path);
103 } else {
104 LOGGER.debug("Serving '{}' headers only", path);
105 }
106 }
107
108 if (response.isCommitted()) {
109
110 return;
111 }
112
113 final boolean isError = response.getStatus() >= HttpServletResponse.SC_BAD_REQUEST;
114
115 if (content == null && !isError) {
116 response.sendError(HttpServletResponse.SC_NOT_FOUND, request.getRequestURI());
117 return;
118 }
119
120
121 if (!isError && !checkIfHeaders(request, response, content)) {
122 return;
123 }
124
125
126 String contentType = content.getMimeType();
127 final String filename = getFileName(request, content);
128 if (contentType == null) {
129 contentType = context.getMimeType(filename);
130 content.setMimeType(contentType);
131 }
132 String enc = content.getEncoding();
133 if (enc != null) {
134 contentType = String.format(Locale.ROOT, "%s; charset=%s", contentType, enc);
135 }
136
137 String eTag = null;
138 List<Range> ranges = null;
139 if (!isError) {
140 eTag = content.getETag();
141 if (config.useAcceptRanges) {
142 response.setHeader("Accept-Ranges", "bytes");
143 }
144
145 ranges = parseRange(request, response, content);
146
147 response.setHeader("ETag", eTag);
148
149 long lastModified = content.lastModified();
150 if (lastModified >= 0) {
151 response.setDateHeader("Last-Modified", lastModified);
152 }
153 if (serveContent) {
154 String dispositionType = request.getParameter("dl") == null ? "inline" : "attachment";
155 response.setHeader("Content-Disposition", dispositionType + ";filename=\"" + filename + "\"");
156 }
157 }
158
159 final long contentLength = content.length();
160
161 if (contentLength == 0) {
162 serveContent = false;
163 }
164
165 if (content.isUsingSession()) {
166 response.addHeader("Cache-Control", "private, max-age=0, must-revalidate");
167 response.addHeader("Vary", "*");
168 }
169
170 try (ServletOutputStream out = serveContent ? response.getOutputStream() : null) {
171 if (serveContent) {
172 try {
173 response.setBufferSize(config.outputBufferSize);
174 } catch (final IllegalStateException e) {
175
176 }
177 }
178 if (response instanceof ServletResponseWrapper) {
179 if (request.getHeader("Range") != null) {
180 LOGGER.warn("Response is wrapped by ServletResponseWrapper, no 'Range' requests supported.");
181 }
182 ranges = ContentUtils.FULL;
183 }
184
185 if (isError || (ranges == null || ranges.isEmpty()) && request.getHeader("Range") == null
186 || ranges == ContentUtils.FULL) {
187
188 if (contentType != null) {
189 if (LOGGER.isDebugEnabled()) {
190 LOGGER.debug("contentType='{}'", contentType);
191 }
192 response.setContentType(contentType);
193 }
194 if (contentLength >= 0) {
195 if (LOGGER.isDebugEnabled()) {
196 LOGGER.debug("contentLength={}", contentLength);
197 }
198 setContentLengthLong(response, contentLength);
199 }
200
201 if (serveContent) {
202 ContentUtils.copy(content, out, config.inputBufferSize, config.outputBufferSize);
203 }
204
205 } else {
206
207 if (ranges == null || ranges.isEmpty()) {
208 return;
209 }
210
211
212
213 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
214
215 if (ranges.size() == 1) {
216
217 final Range range = ranges.get(0);
218 response.addHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);
219 final long length = range.end - range.start + 1;
220 setContentLengthLong(response, length);
221
222 if (contentType != null) {
223 if (LOGGER.isDebugEnabled()) {
224 LOGGER.debug("contentType='{}'", contentType);
225 }
226 response.setContentType(contentType);
227 }
228
229 if (serveContent) {
230 ContentUtils.copy(content, out, range, config.inputBufferSize, config.outputBufferSize);
231 }
232
233 } else {
234
235 response.setContentType("multipart/byteranges; boundary=" + ContentUtils.MIME_BOUNDARY);
236
237 if (serveContent) {
238 ContentUtils.copy(content, out, ranges.iterator(), contentType, config.inputBufferSize,
239 config.outputBufferSize);
240 }
241 }
242 }
243 }
244 }
245
246
247
248
249
250 private static boolean checkIfHeaders(final HttpServletRequest request, final HttpServletResponse response,
251 final MCRContent content) throws IOException {
252
253 return checkIfMatch(request, response, content) && checkIfModifiedSince(request, response, content)
254 && checkIfNoneMatch(request, response, content) && checkIfUnmodifiedSince(request, response, content);
255
256 }
257
258
259
260
261 private static boolean checkIfMatch(final HttpServletRequest request, final HttpServletResponse response,
262 final MCRContent content) throws IOException {
263
264 final String eTag = content.getETag();
265 final String headerValue = request.getHeader("If-Match");
266 if (headerValue != null) {
267 if (headerValue.indexOf('*') == -1) {
268
269 final StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
270 boolean conditionSatisfied = false;
271
272 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
273 final String currentToken = commaTokenizer.nextToken();
274 if (currentToken.trim().equals(eTag)) {
275 conditionSatisfied = true;
276 }
277 }
278
279
280 if (!conditionSatisfied) {
281 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
282 return false;
283 }
284
285 }
286 }
287 return true;
288 }
289
290
291
292
293
294 private static boolean checkIfModifiedSince(final HttpServletRequest request, final HttpServletResponse response,
295 final MCRContent content) throws IOException {
296 try {
297 final long headerValue = request.getDateHeader("If-Modified-Since");
298 final long lastModified = content.lastModified();
299 if (headerValue != -1) {
300
301
302
303 if (request.getHeader("If-None-Match") == null && lastModified < headerValue + 1000) {
304 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
305 response.setHeader("ETag", content.getETag());
306
307 return false;
308 }
309 }
310 } catch (final IllegalArgumentException illegalArgument) {
311 return true;
312 }
313 return true;
314 }
315
316
317
318
319 private static boolean checkIfNoneMatch(final HttpServletRequest request, final HttpServletResponse response,
320 final MCRContent content) throws IOException {
321
322 final String eTag = content.getETag();
323 final String headerValue = request.getHeader("If-None-Match");
324 if (headerValue != null) {
325
326 boolean conditionSatisfied = false;
327
328 if ("*".equals(headerValue)) {
329 conditionSatisfied = true;
330 } else {
331 final StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
332 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
333 final String currentToken = commaTokenizer.nextToken();
334 if (currentToken.trim().equals(eTag)) {
335 conditionSatisfied = true;
336 }
337 }
338
339 }
340
341 if (conditionSatisfied) {
342
343 if ("GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod())) {
344 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
345 response.setHeader("ETag", eTag);
346
347 return false;
348 }
349 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
350 return false;
351 }
352 }
353 return true;
354 }
355
356
357
358
359 private static boolean checkIfUnmodifiedSince(final HttpServletRequest request, final HttpServletResponse response,
360 final MCRContent resource) throws IOException {
361 try {
362 final long lastModified = resource.lastModified();
363 final long headerValue = request.getDateHeader("If-Unmodified-Since");
364 if (headerValue != -1) {
365 if (lastModified >= headerValue + 1000) {
366
367 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
368 return false;
369 }
370 }
371 } catch (final IllegalArgumentException illegalArgument) {
372 return true;
373 }
374 return true;
375 }
376
377 public static long copyLarge(InputStream input, OutputStream output, long inputOffset, long length, byte[] buffer)
378 throws IOException {
379 return ContentUtils.copyLarge(input, output, inputOffset, length, buffer);
380 }
381
382 private static String extractFileName(String filename) {
383 int filePosition = filename.lastIndexOf('/') + 1;
384 filename = filename.substring(filePosition);
385 filePosition = filename.lastIndexOf('.');
386 if (filePosition > 0) {
387 filename = filename.substring(0, filePosition);
388 }
389 return filename;
390 }
391
392 private static String getFileName(final HttpServletRequest req, final MCRContent content) {
393 final String filename = content.getName();
394 if (filename != null) {
395 return filename;
396 }
397 if (req.getPathInfo() != null) {
398 return extractFileName(req.getPathInfo());
399 }
400 return new MessageFormat("{0}-{1}", Locale.ROOT).format(
401 new Object[] { extractFileName(req.getServletPath()), System.currentTimeMillis() });
402 }
403
404
405
406
407 private static String getRequestPath(final HttpServletRequest request) {
408 return request.getServletPath() + (request.getPathInfo() == null ? "" : request.getPathInfo());
409 }
410
411
412
413
414
415
416 private static List<Range> parseRange(final HttpServletRequest request, final HttpServletResponse response,
417 final MCRContent content) throws IOException {
418
419
420 final String headerValue = request.getHeader("If-Range");
421
422 if (headerValue != null) {
423 long headerValueTime = -1L;
424 try {
425 headerValueTime = request.getDateHeader("If-Range");
426 } catch (final IllegalArgumentException e) {
427
428 }
429
430 final String eTag = content.getETag();
431 final long lastModified = content.lastModified();
432
433 if (headerValueTime == -1L) {
434
435 if (!eTag.equals(headerValue.trim())) {
436 return ContentUtils.FULL;
437 }
438 } else {
439
440 if (lastModified > headerValueTime + 1000) {
441 return ContentUtils.FULL;
442 }
443 }
444
445 }
446
447 final long fileLength = content.length();
448 if (fileLength <= 0) {
449 return null;
450 }
451
452 String rangeHeader = request.getHeader("Range");
453 try {
454 return Range.parseRanges(rangeHeader, fileLength);
455 } catch (IllegalArgumentException e) {
456 response.addHeader("Content-Range", "bytes */" + fileLength);
457 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
458 return null;
459 }
460 }
461
462 private static void setContentLengthLong(final HttpServletResponse response, final long length) {
463 response.setHeader("Content-Length", String.valueOf(length));
464 }
465
466 public static class Config {
467
468 public boolean useAcceptRanges = true;
469
470 public int inputBufferSize = ContentUtils.DEFAULT_BUFFER_SIZE;
471
472 public int outputBufferSize = ContentUtils.DEFAULT_BUFFER_SIZE;
473
474 }
475
476 }