1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.restapi.v2;
20
21 import static org.mycore.restapi.v2.MCRRestAuthorizationFilter.PARAM_DERID;
22 import static org.mycore.restapi.v2.MCRRestAuthorizationFilter.PARAM_DER_PATH;
23 import static org.mycore.restapi.v2.MCRRestAuthorizationFilter.PARAM_MCRID;
24
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.io.OutputStream;
28 import java.io.UncheckedIOException;
29 import java.nio.file.DirectoryNotEmptyException;
30 import java.nio.file.DirectoryStream;
31 import java.nio.file.FileAlreadyExistsException;
32 import java.nio.file.Files;
33 import java.nio.file.SecureDirectoryStream;
34 import java.nio.file.StandardOpenOption;
35 import java.nio.file.attribute.BasicFileAttributes;
36 import java.util.ArrayList;
37 import java.util.Base64;
38 import java.util.Date;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Optional;
42 import java.util.concurrent.TimeUnit;
43 import java.util.function.Function;
44 import java.util.stream.Collectors;
45 import java.util.stream.StreamSupport;
46
47 import org.apache.commons.io.IOUtils;
48 import org.apache.commons.io.output.CountingOutputStream;
49 import org.apache.commons.io.output.DeferredFileOutputStream;
50 import org.apache.logging.log4j.LogManager;
51 import org.mycore.common.MCRSession;
52 import org.mycore.common.MCRSessionMgr;
53 import org.mycore.common.MCRTransactionHelper;
54 import org.mycore.common.config.MCRConfiguration2;
55 import org.mycore.common.content.MCRPathContent;
56 import org.mycore.common.content.util.MCRRestContentHelper;
57 import org.mycore.datamodel.metadata.MCRObjectID;
58 import org.mycore.datamodel.niofs.MCRFileAttributes;
59 import org.mycore.datamodel.niofs.MCRMD5AttributeView;
60 import org.mycore.datamodel.niofs.MCRPath;
61 import org.mycore.datamodel.niofs.utils.MCRRecursiveDeleter;
62 import org.mycore.frontend.jersey.MCRCacheControl;
63 import org.mycore.restapi.annotations.MCRRequireTransaction;
64
65 import com.fasterxml.jackson.annotation.JsonAutoDetect;
66 import com.fasterxml.jackson.annotation.JsonFormat;
67 import com.fasterxml.jackson.annotation.JsonInclude;
68 import com.fasterxml.jackson.annotation.JsonProperty;
69 import com.google.common.collect.Ordering;
70
71 import io.swagger.v3.oas.annotations.Operation;
72 import io.swagger.v3.oas.annotations.Parameter;
73 import io.swagger.v3.oas.annotations.enums.ParameterIn;
74 import io.swagger.v3.oas.annotations.headers.Header;
75 import io.swagger.v3.oas.annotations.media.Schema;
76 import io.swagger.v3.oas.annotations.responses.ApiResponse;
77 import jakarta.servlet.ServletContext;
78 import jakarta.ws.rs.Consumes;
79 import jakarta.ws.rs.DELETE;
80 import jakarta.ws.rs.DefaultValue;
81 import jakarta.ws.rs.GET;
82 import jakarta.ws.rs.HEAD;
83 import jakarta.ws.rs.PUT;
84 import jakarta.ws.rs.Path;
85 import jakarta.ws.rs.PathParam;
86 import jakarta.ws.rs.Produces;
87 import jakarta.ws.rs.container.ContainerRequestContext;
88 import jakarta.ws.rs.core.Context;
89 import jakarta.ws.rs.core.EntityTag;
90 import jakarta.ws.rs.core.HttpHeaders;
91 import jakarta.ws.rs.core.MediaType;
92 import jakarta.ws.rs.core.MultivaluedMap;
93 import jakarta.ws.rs.core.Response;
94 import jakarta.ws.rs.core.UriInfo;
95 import jakarta.ws.rs.core.Variant;
96 import jakarta.xml.bind.annotation.XmlAccessType;
97 import jakarta.xml.bind.annotation.XmlAccessorType;
98 import jakarta.xml.bind.annotation.XmlAttribute;
99 import jakarta.xml.bind.annotation.XmlElement;
100 import jakarta.xml.bind.annotation.XmlRootElement;
101
102 @Path("/objects/{" + PARAM_MCRID + "}/derivates/{" + PARAM_DERID + "}/contents{" + PARAM_DER_PATH + ":(/[^/]+)*}")
103 public class MCRRestDerivateContents {
104 private static final String HTTP_HEADER_IS_DIRECTORY = "X-MCR-IsDirectory";
105
106 private static final int BUFFER_SIZE = 8192;
107
108 @Context
109 ContainerRequestContext request;
110
111 @Context
112 ServletContext context;
113
114 @Parameter(example = "mir_mods_00004711")
115 @PathParam(PARAM_MCRID)
116 MCRObjectID mcrId;
117
118 @Parameter(example = "mir_derivate_00004711")
119 @PathParam(PARAM_DERID)
120 MCRObjectID derid;
121
122 @PathParam(PARAM_DER_PATH)
123 @DefaultValue("")
124 String path;
125
126 private static Response createDirectory(MCRPath mcrPath) {
127 try {
128 BasicFileAttributes directoryAttrs = Files.readAttributes(mcrPath, BasicFileAttributes.class);
129 if (!directoryAttrs.isDirectory()) {
130 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
131 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_CREATE_DIRECTORY_ON_FILE)
132 .withMessage("Could not create directory " + mcrPath + ". A file allready exist!")
133 .toException();
134 }
135 return Response.noContent().build();
136 } catch (IOException e) {
137
138 LogManager.getLogger().info("Creating directory: {}", mcrPath);
139 try {
140 doWithinTransaction(() -> Files.createDirectory(mcrPath));
141 } catch (IOException e2) {
142 throw MCRErrorResponse.fromStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
143 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_CREATE_DIRECTORY)
144 .withMessage("Could not create directory " + mcrPath + ".")
145 .withDetail(e.getMessage())
146 .withCause(e)
147 .toException();
148 }
149 return Response.status(Response.Status.CREATED).build();
150 }
151 }
152
153 private static void doWithinTransaction(IOOperation op) throws IOException {
154 MCRSession mcrSession = MCRSessionMgr.getCurrentSession();
155 try {
156 MCRTransactionHelper.beginTransaction();
157 op.run();
158 } finally {
159 if (MCRTransactionHelper.transactionRequiresRollback()) {
160 MCRTransactionHelper.rollbackTransaction();
161 } else {
162 MCRTransactionHelper.commitTransaction();
163 }
164 }
165 }
166
167 private static Response updateFile(InputStream contents, MCRPath mcrPath) {
168 LogManager.getLogger().info("Updating file: {}", mcrPath);
169 int memBuf = getUploadMemThreshold();
170 java.io.File uploadDirectory = getUploadTempStorage();
171 try (DeferredFileOutputStream dfos = new DeferredFileOutputStream(memBuf, mcrPath.getOwner(),
172 mcrPath.getFileName().toString(), uploadDirectory);
173 MaxBytesOutputStream mbos = new MaxBytesOutputStream(dfos)) {
174 IOUtils.copy(contents, mbos);
175 mbos.close();
176 OutputStream out = Files.newOutputStream(mcrPath);
177 try {
178 if (dfos.isInMemory()) {
179 out.write(dfos.getData());
180 } else {
181 java.io.File tempFile = dfos.getFile();
182 if (tempFile != null) {
183 try {
184 Files.copy(tempFile.toPath(), out);
185 } finally {
186 LogManager.getLogger().debug("Deleting file {} of size {}.", tempFile.getAbsolutePath(),
187 tempFile.length());
188 tempFile.delete();
189 }
190 }
191 }
192 } finally {
193
194 doWithinTransaction(out::close);
195 }
196 } catch (IOException e) {
197 throw MCRErrorResponse.fromStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
198 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_UPDATE_FILE)
199 .withMessage("Could not update file " + mcrPath + ".")
200 .withDetail(e.getMessage())
201 .withCause(e)
202 .toException();
203 }
204 return Response.noContent().build();
205 }
206
207 private static Response createFile(InputStream contents, MCRPath mcrPath) {
208 LogManager.getLogger().info("Creating file: {}", mcrPath);
209 try {
210 OutputStream out = Files.newOutputStream(mcrPath, StandardOpenOption.CREATE_NEW);
211 try {
212 IOUtils.copy(contents, out, BUFFER_SIZE);
213 } finally {
214
215 doWithinTransaction(out::close);
216 }
217 } catch (IOException e) {
218 try {
219 doWithinTransaction(() -> Files.deleteIfExists(mcrPath));
220 } catch (IOException e2) {
221 LogManager.getLogger().warn("Error while deleting incomplete file.", e2);
222 }
223 throw MCRErrorResponse.fromStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
224 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_CREATE_DIRECTORY)
225 .withMessage("Could not create file " + mcrPath + ".")
226 .withDetail(e.getMessage())
227 .withCause(e)
228 .toException();
229 }
230 return Response.status(Response.Status.CREATED).build();
231 }
232
233 private static EntityTag getETag(MCRFileAttributes attrs) {
234 return new EntityTag(attrs.md5sum());
235 }
236
237 private static long getUploadMaxSize() {
238 return MCRConfiguration2.getOrThrow("MCR.FileUpload.MaxSize", Long::parseLong);
239 }
240
241 private static java.io.File getUploadTempStorage() {
242 return MCRConfiguration2.getOrThrow("MCR.FileUpload.TempStoragePath", java.io.File::new);
243 }
244
245 private static int getUploadMemThreshold() {
246 return MCRConfiguration2.getOrThrow("MCR.FileUpload.MemoryThreshold", Integer::parseInt);
247 }
248
249
250
251
252
253
254 private static String getDigestHeader(String md5sum) {
255 final String md5Base64 = Base64.getEncoder().encodeToString(getMD5Digest(md5sum));
256 return "MD5=" + md5Base64;
257 }
258
259 private static byte[] getMD5Digest(String md5sum) {
260 final char[] data = md5sum.toCharArray();
261 final int len = data.length;
262
263
264 final byte[] md5Bytes = new byte[len >> 1];
265 for (int i = 0, j = 0; j < len; i++) {
266 int f = Character.digit(data[j], 16) << 4;
267 j++;
268 f = f | Character.digit(data[j], 16);
269 j++;
270 md5Bytes[i] = (byte) (f & 0xFF);
271 }
272 return md5Bytes;
273 }
274
275 @HEAD
276 @MCRCacheControl(sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
277 @Operation(description = "get information about mime-type(s), last modified, ETag (md5sum) and ranges support",
278 tags = MCRRestUtils.TAG_MYCORE_FILE,
279 responses = @ApiResponse(
280 description = "Use this for single file metadata queries only. Support is implemented for user agents.",
281 headers = {
282 @Header(name = "Content-Type", description = "mime type of file"),
283 @Header(name = "Content-Length", description = "size of file"),
284 @Header(name = "ETag", description = "MD5 sum of file"),
285 @Header(name = "Last-Modified", description = "last modified date of file"),
286 }))
287 public Response getFileOrDirectoryMetadata() {
288 MCRPath mcrPath = getPath();
289 MCRFileAttributes fileAttributes;
290 try {
291 fileAttributes = Files.readAttributes(mcrPath, MCRFileAttributes.class);
292 } catch (IOException e) {
293 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
294 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_NOT_FOUND)
295 .withMessage("Could not find file or directory " + mcrPath + ".")
296 .withDetail(e.getMessage())
297 .withCause(e)
298 .toException();
299 }
300 if (fileAttributes.isDirectory()) {
301 return Response.ok()
302 .variants(Variant
303 .mediaTypes(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE)
304 .build())
305 .build();
306 }
307 String mimeType = context.getMimeType(path);
308 return Response
309 .status(Response.Status.PARTIAL_CONTENT)
310 .header("Accept-Ranges", "bytes")
311 .header(HttpHeaders.CONTENT_TYPE, mimeType)
312 .lastModified(Date.from(fileAttributes.lastModifiedTime().toInstant()))
313 .header(HttpHeaders.CONTENT_LENGTH, fileAttributes.size())
314 .tag(getETag(fileAttributes))
315 .header("Digest", getDigestHeader(fileAttributes.md5sum()))
316 .build();
317 }
318
319 @GET
320 @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON + ";charset=UTF-8",
321 MediaType.WILDCARD })
322 @MCRCacheControl(maxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS),
323 sMaxAge = @MCRCacheControl.Age(time = 1, unit = TimeUnit.DAYS))
324 @Operation(
325 summary = "List directory contents or serves file given by {path} in derivate",
326 tags = MCRRestUtils.TAG_MYCORE_FILE)
327 public Response getFileOrDirectory(@Context UriInfo uriInfo, @Context HttpHeaders requestHeader) {
328 LogManager.getLogger().info("{}:{}", derid, path);
329 MCRPath mcrPath = MCRPath.getPath(derid.toString(), path);
330 MCRFileAttributes fileAttributes;
331 try {
332 fileAttributes = Files.readAttributes(mcrPath, MCRFileAttributes.class);
333 } catch (IOException e) {
334 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
335 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_NOT_FOUND)
336 .withMessage("Could not find file or directory " + mcrPath + ".")
337 .withDetail(e.getMessage())
338 .withCause(e)
339 .toException();
340 }
341 Date lastModified = new Date(fileAttributes.lastModifiedTime().toMillis());
342 if (fileAttributes.isDirectory()) {
343 return MCRRestUtils
344 .getCachedResponse(request.getRequest(), lastModified)
345 .orElseGet(() -> serveDirectory(mcrPath, fileAttributes));
346 }
347 return MCRRestUtils
348 .getCachedResponse(request.getRequest(), lastModified, getETag(fileAttributes))
349 .orElseGet(() -> {
350 MCRPathContent content = new MCRPathContent(mcrPath, fileAttributes);
351 content.setMimeType(context.getMimeType(mcrPath.getFileName().toString()));
352 try {
353 final List<Map.Entry<String, String>> responseHeader = List
354 .of(Map.entry("Digest", getDigestHeader(fileAttributes.md5sum())));
355 return MCRRestContentHelper.serveContent(content, uriInfo, requestHeader, responseHeader);
356 } catch (IOException e) {
357 throw MCRErrorResponse.fromStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
358 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_IO_ERROR)
359 .withMessage("Could not send file " + mcrPath + ".")
360 .withDetail(e.getMessage())
361 .withCause(e)
362 .toException();
363 }
364 });
365 }
366
367 @PUT
368 @Consumes(MediaType.WILDCARD)
369 @Operation(summary = "Creates directory or file. Parent directories will be created if they do not exist.",
370 parameters = {
371 @Parameter(in = ParameterIn.HEADER,
372 name = HTTP_HEADER_IS_DIRECTORY,
373 description = "set to 'true' if a new directory should be created",
374 required = false,
375 schema = @Schema(type = "boolean")) },
376 responses = {
377 @ApiResponse(responseCode = "204", description = "if directory already exists or while was updated"),
378 @ApiResponse(responseCode = "201", description = "if directory or file was created"),
379 @ApiResponse(responseCode = "400",
380 description = "if directory overwrites file or vice versa; content length is too big"),
381 },
382 tags = MCRRestUtils.TAG_MYCORE_FILE)
383 public Response createFileOrDirectory(InputStream contents) {
384 MCRPath mcrPath = MCRPath.getPath(derid.toString(), path);
385 if (mcrPath.getNameCount() > 1) {
386 MCRPath parentDirectory = mcrPath.getParent();
387 try {
388 Files.createDirectories(parentDirectory);
389 } catch (FileAlreadyExistsException e) {
390 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
391 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NOT_DIRECTORY)
392 .withMessage("A file " + parentDirectory + " exists and can not be used as parent directory.")
393 .withDetail(e.getMessage())
394 .withCause(e)
395 .toException();
396 } catch (IOException e) {
397 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
398 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_NOT_FOUND)
399 .withMessage("Could not find directory " + parentDirectory + ".")
400 .withDetail(e.getMessage())
401 .withCause(e)
402 .toException();
403 }
404 }
405 if (isFile()) {
406 long maxSize = getUploadMaxSize();
407 String contentLength = request.getHeaderString(HttpHeaders.CONTENT_LENGTH);
408 if (contentLength != null && Long.parseLong(contentLength) > maxSize) {
409 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
410 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_SIZE)
411 .withMessage("Maximum file size (" + maxSize + " bytes) exceeded.")
412 .withDetail(contentLength)
413 .toException();
414 }
415 return updateOrCreateFile(contents, mcrPath);
416 } else {
417
418 return createDirectory(mcrPath);
419 }
420 }
421
422 @DELETE
423 @Operation(summary = "Deletes file or directory.",
424 responses = { @ApiResponse(responseCode = "204", description = "if deletion was successful")
425 },
426 tags = MCRRestUtils.TAG_MYCORE_FILE)
427 @MCRRequireTransaction
428 public Response deleteFileOrDirectory() {
429 MCRPath mcrPath = getPath();
430 try {
431 if (Files.exists(mcrPath) && Files.isDirectory(mcrPath)) {
432
433 Files.walkFileTree(mcrPath, MCRRecursiveDeleter.instance());
434 return Response.noContent().build();
435 } else if (Files.deleteIfExists(mcrPath)) {
436 return Response.noContent().build();
437 }
438 } catch (DirectoryNotEmptyException e) {
439 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
440 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_DIRECTORY_NOT_EMPTY)
441 .withMessage("Directory " + mcrPath + " is not empty.")
442 .withDetail(e.getMessage())
443 .withCause(e)
444 .toException();
445 } catch (IOException e) {
446 throw MCRErrorResponse.fromStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
447 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_DELETE)
448 .withMessage("Could not delete file or directory " + mcrPath + ".")
449 .withDetail(e.getMessage())
450 .withCause(e)
451 .toException();
452 }
453 throw MCRErrorResponse.fromStatus(Response.Status.NOT_FOUND.getStatusCode())
454 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_NOT_FOUND)
455 .withMessage("Could not find file or directory " + mcrPath + ".")
456 .toException();
457 }
458
459 private Response updateOrCreateFile(InputStream contents, MCRPath mcrPath) {
460 MCRFileAttributes fileAttributes;
461 try {
462 fileAttributes = Files.readAttributes(mcrPath, MCRFileAttributes.class);
463 if (!fileAttributes.isRegularFile()) {
464 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
465 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_NOT_FILE)
466 .withMessage(mcrPath + " is not a file.")
467 .toException();
468 }
469 } catch (IOException e) {
470
471 return createFile(contents, mcrPath);
472 }
473
474 Date lastModified = new Date(fileAttributes.lastModifiedTime().toMillis());
475 EntityTag eTag = getETag(fileAttributes);
476 Optional<Response> cachedResponse = MCRRestUtils.getCachedResponse(request.getRequest(), lastModified,
477 eTag);
478 if (cachedResponse.isPresent()) {
479 return cachedResponse.get();
480 }
481 return updateFile(contents, mcrPath);
482 }
483
484 private boolean isFile() {
485
486 MultivaluedMap<String, String> headers = request.getHeaders();
487 return ((!"true".equalsIgnoreCase(headers.getFirst(HTTP_HEADER_IS_DIRECTORY))))
488 && !request.getUriInfo().getPath().endsWith("/")
489 && (headers.containsKey(HttpHeaders.CONTENT_LENGTH) || headers.containsKey("Transfer-Encoding"));
490 }
491
492 private Response serveDirectory(MCRPath mcrPath, MCRFileAttributes dirAttrs) {
493 Directory dir = new Directory(mcrPath, dirAttrs);
494 try (DirectoryStream ds = Files.newDirectoryStream(mcrPath)) {
495
496 Function<MCRPath, MCRFileAttributes> attrResolver = p -> {
497 try {
498 return (ds instanceof SecureDirectoryStream)
499 ? ((SecureDirectoryStream<MCRPath>) ds).getFileAttributeView(MCRPath.toMCRPath(p.getFileName()),
500 MCRMD5AttributeView.class).readAllAttributes()
501 : Files.readAttributes(p, MCRFileAttributes.class);
502 } catch (IOException e) {
503 throw new UncheckedIOException(e);
504 }
505 };
506 List<DirectoryEntry> entries = StreamSupport
507 .stream(((DirectoryStream<MCRPath>) ds).spliterator(), false)
508 .collect(Collectors.toMap(p -> p, attrResolver))
509 .entrySet()
510 .stream()
511 .map(e -> e.getValue().isDirectory() ? new Directory(e.getKey(), e.getValue())
512 : new File(e.getKey(), e.getValue(), context.getMimeType(e.getKey().getFileName().toString())))
513 .sorted()
514 .collect(Collectors.toList());
515 dir.setEntries(entries);
516 } catch (IOException | UncheckedIOException e) {
517 throw MCRErrorResponse.fromStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())
518 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_IO_ERROR)
519 .withMessage("Could not send directory " + mcrPath + ".")
520 .withDetail(e.getMessage())
521 .withCause(e)
522 .toException();
523 }
524 return Response.ok(dir).lastModified(new Date(dirAttrs.lastModifiedTime().toMillis())).build();
525 }
526
527 private MCRPath getPath() {
528 return MCRPath.getPath(derid.toString(), path);
529 }
530
531 @FunctionalInterface
532 private interface IOOperation {
533 void run() throws IOException;
534 }
535
536 @XmlRootElement(name = "directory")
537 @XmlAccessorType(XmlAccessType.PROPERTY)
538 @JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY)
539 @JsonInclude(content = JsonInclude.Include.NON_EMPTY)
540 private static class Directory extends DirectoryEntry {
541 private List<Directory> dirs;
542
543 private List<File> files;
544
545 Directory() {
546 super();
547 }
548
549 Directory(MCRPath p, MCRFileAttributes attr) {
550 super(p, attr);
551 }
552
553 void setEntries(List<? extends DirectoryEntry> entries) {
554 LogManager.getLogger().info(entries);
555 dirs = new ArrayList<>();
556 files = new ArrayList<>();
557 entries.stream()
558 .collect(Collectors.groupingBy(Object::getClass))
559 .forEach((c, e) -> {
560 if (File.class.isAssignableFrom(c)) {
561 files.addAll((List<File>) e);
562 } else if (Directory.class.isAssignableFrom(c)) {
563 dirs.addAll((List<Directory>) e);
564 }
565
566 });
567 }
568
569 @XmlElement(name = "directory")
570 @JsonProperty("directories")
571 @JsonInclude(content = JsonInclude.Include.NON_EMPTY)
572 public List<Directory> getDirs() {
573 return dirs;
574 }
575
576 @XmlElement(name = "file")
577 @JsonProperty("files")
578 @JsonInclude(content = JsonInclude.Include.NON_EMPTY)
579 public List<File> getFiles() {
580 return files;
581 }
582
583 }
584
585 private static class File extends DirectoryEntry {
586
587 private String mimeType;
588
589 private String md5;
590
591 private long size;
592
593 File() {
594 super();
595 }
596
597 File(MCRPath p, MCRFileAttributes attr, String mimeType) {
598 super(p, attr);
599 this.md5 = attr.md5sum();
600 this.size = attr.size();
601 this.mimeType = mimeType;
602 }
603
604 @XmlAttribute
605 @JsonProperty(index = 3)
606 public String getMd5() {
607 return md5;
608 }
609
610 @XmlAttribute
611 @JsonProperty(index = 1)
612 public long getSize() {
613 return size;
614 }
615
616 @XmlAttribute
617 @JsonProperty(index = 4)
618 public String getMimeType() {
619 return mimeType;
620 }
621 }
622
623 @JsonInclude(content = JsonInclude.Include.NON_NULL)
624 private abstract static class DirectoryEntry implements Comparable<DirectoryEntry> {
625 private String name;
626
627 private Date modified;
628
629 DirectoryEntry(MCRPath p, MCRFileAttributes attr) {
630 this.name = Optional.ofNullable(p.getFileName())
631 .map(java.nio.file.Path::toString)
632 .orElse(null);
633 this.modified = Date.from(attr.lastModifiedTime().toInstant());
634 }
635
636 DirectoryEntry() {
637 }
638
639 @XmlAttribute
640 @JsonProperty(index = 0)
641 @JsonInclude(content = JsonInclude.Include.NON_EMPTY)
642 String getName() {
643 return name;
644 }
645
646 @XmlAttribute
647 @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = MCRRestUtils.JSON_DATE_FORMAT)
648 @JsonProperty(index = 2)
649 public Date getModified() {
650 return modified;
651 }
652
653 @Override
654 public int compareTo(DirectoryEntry o) {
655 return Ordering
656 .<DirectoryEntry>from((de1, de2) -> {
657 if (de1 instanceof Directory && !(de2 instanceof Directory)) {
658 return -1;
659 }
660 if (de1.getClass().equals(de2.getClass())) {
661 return 0;
662 }
663 return 1;
664 })
665 .compound((de1, de2) -> de1.getName().compareTo(de2.getName()))
666 .compare(this, o);
667 }
668 }
669
670 private static class MaxBytesOutputStream extends CountingOutputStream {
671
672 private final long maxSize;
673
674 MaxBytesOutputStream(OutputStream out) {
675 super(out);
676 maxSize = getUploadMaxSize();
677 }
678
679 @Override
680 protected synchronized void beforeWrite(int n) {
681 super.beforeWrite(n);
682 if (getByteCount() > maxSize) {
683 throw MCRErrorResponse.fromStatus(Response.Status.BAD_REQUEST.getStatusCode())
684 .withErrorCode(MCRErrorCodeConstants.MCRDERIVATE_FILE_SIZE)
685 .withMessage("Maximum file size (" + maxSize + " bytes) exceeded.")
686 .toException();
687 }
688 }
689 }
690
691 }