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.util.Date;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
29
30 import org.apache.logging.log4j.LogManager;
31 import org.apache.logging.log4j.Logger;
32 import org.mycore.common.content.MCRContent;
33
34 import com.google.common.collect.Iterables;
35
36 import jakarta.ws.rs.NotFoundException;
37 import jakarta.ws.rs.WebApplicationException;
38 import jakarta.ws.rs.core.HttpHeaders;
39 import jakarta.ws.rs.core.MediaType;
40 import jakarta.ws.rs.core.Response;
41 import jakarta.ws.rs.core.StreamingOutput;
42 import jakarta.ws.rs.core.UriInfo;
43 import jakarta.ws.rs.ext.RuntimeDelegate;
44
45
46
47
48 public abstract class MCRRestContentHelper {
49
50 public static final RuntimeDelegate.HeaderDelegate<Date> DATE_HEADER_DELEGATE = RuntimeDelegate.getInstance()
51 .createHeaderDelegate(Date.class);
52
53 private static Logger LOGGER = LogManager.getLogger();
54
55 public static Response serveContent(final MCRContent content, final UriInfo uriInfo,
56 final HttpHeaders requestHeader, List<Map.Entry<String, String>> responseHeader)
57 throws IOException {
58 return serveContent(content, uriInfo, requestHeader, responseHeader, new Config());
59 }
60
61 public static Response serveContent(final MCRContent content, final UriInfo uriInfo,
62 final HttpHeaders requestHeader, final List<Map.Entry<String, String>> responseHeader, final Config config)
63 throws IOException {
64
65 if (content == null) {
66 throw new NotFoundException();
67 }
68
69
70 MediaType contentType = getMediaType(content);
71
72 Response.ResponseBuilder response = Response.ok();
73
74 String eTag = content.getETag();
75 response.header(HttpHeaders.ETAG, eTag);
76 responseHeader.forEach(e -> response.header(e.getKey(), e.getValue()));
77 final long contentLength = content.length();
78 if (contentLength == 0) {
79
80 return response.status(Response.Status.NO_CONTENT).build();
81 }
82 long lastModified = content.lastModified();
83 if (lastModified >= 0) {
84 response.lastModified(new Date(lastModified));
85 }
86
87 List<Range> ranges = null;
88 if (config.useAcceptRanges) {
89 response.header("Accept-Ranges", "bytes");
90 ranges = parseRange(requestHeader, lastModified, eTag, contentLength);
91 String varyHeader = Stream.of("Range", "If-Range")
92 .filter(h -> requestHeader.getHeaderString(h) != null)
93 .collect(Collectors.joining(","));
94 if (!varyHeader.isEmpty()) {
95 response.header(HttpHeaders.VARY, varyHeader);
96 }
97 }
98
99 String filename = Optional.of(content.getName())
100 .orElseGet(() -> Iterables.getLast(uriInfo.getPathSegments()).getPath());
101 response.header(HttpHeaders.CONTENT_DISPOSITION,
102 config.dispositionType.name() + ";filename=\"" + filename + "\"");
103
104 boolean noRangeRequest = ranges == null || ranges == ContentUtils.FULL;
105 if (noRangeRequest) {
106 LOGGER.debug("contentType='{}'", contentType);
107 LOGGER.debug("contentLength={}", contentLength);
108 response.type(contentType);
109 response.header(HttpHeaders.CONTENT_LENGTH, contentLength);
110 response.entity(
111 (StreamingOutput) out -> ContentUtils.copy(content, out, config.inputBufferSize,
112 config.outputBufferSize));
113
114 } else if (ranges.isEmpty()) {
115 return response.status(Response.Status.NO_CONTENT).build();
116 } else {
117
118 response.status(Response.Status.PARTIAL_CONTENT);
119
120 if (ranges.size() == 1) {
121 final Range range = ranges.get(0);
122 response.header("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);
123 final long length = range.end - range.start + 1;
124 response.header(HttpHeaders.CONTENT_LENGTH, length);
125
126 LOGGER.debug("contentType='{}'", contentType);
127 response.type(contentType);
128
129 response.entity(
130 (StreamingOutput) out -> ContentUtils.copy(content, out, range, config.inputBufferSize,
131 config.outputBufferSize));
132 } else {
133 response.type("multipart/byteranges; boundary=" + ContentUtils.MIME_BOUNDARY);
134 Iterator<Range> rangeIterator = ranges.iterator();
135 String ct = contentType.toString();
136 response.entity(
137 (StreamingOutput) out -> ContentUtils.copy(content, out, rangeIterator, ct,
138 config.inputBufferSize,
139 config.outputBufferSize));
140 }
141 }
142 return response.build();
143 }
144
145 private static MediaType getMediaType(MCRContent content) throws IOException {
146 String mimeType = content.getMimeType();
147 if (mimeType == null) {
148 mimeType = MediaType.APPLICATION_OCTET_STREAM;
149 }
150 MediaType contentType = MediaType.valueOf(mimeType);
151 String enc = content.getEncoding();
152 if (enc != null) {
153 HashMap<String, String> param = new HashMap<>(contentType.getParameters());
154 param.put(MediaType.CHARSET_PARAMETER, enc);
155 contentType = new MediaType(contentType.getType(), contentType.getSubtype(), param);
156 }
157 return contentType;
158 }
159
160
161
162
163
164
165 private static List<Range> parseRange(HttpHeaders headers, long lastModified, String eTag, long contentLength) {
166
167
168 String ifRangeHeader = headers.getHeaderString("If-Range");
169 if (ifRangeHeader != null) {
170 long headerValueTime = -1L;
171 try {
172 headerValueTime = DATE_HEADER_DELEGATE.fromString(ifRangeHeader).getTime();
173 } catch (final IllegalArgumentException e) {
174
175 }
176
177 if (headerValueTime == -1L) {
178
179 if (!eTag.equals(ifRangeHeader.trim())) {
180 return ContentUtils.FULL;
181 }
182 } else {
183
184 if (lastModified > headerValueTime + 1000) {
185 return ContentUtils.FULL;
186 }
187 }
188
189 }
190 String rangeHeader = headers.getHeaderString("Range");
191 try {
192 return Range.parseRanges(rangeHeader, contentLength);
193 } catch (IllegalArgumentException e) {
194 Response errResponse = Response.status(Response.Status.REQUESTED_RANGE_NOT_SATISFIABLE)
195 .header("Content-Range", "bytes */" + contentLength).build();
196 throw new WebApplicationException(errResponse);
197 }
198 }
199
200 public enum ContentDispositionType {
201 inline, attachment
202 }
203
204 public static class Config {
205
206 public ContentDispositionType dispositionType = ContentDispositionType.attachment;
207
208 public boolean useAcceptRanges = true;
209
210 public int inputBufferSize = ContentUtils.DEFAULT_BUFFER_SIZE;
211
212 public int outputBufferSize = ContentUtils.DEFAULT_BUFFER_SIZE;
213
214 }
215
216 }