View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
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             //does not exist
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(); //required if temporary file was used
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                 //close writes data to database
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                 //close writes data to database
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      * Generate Digest header value.
251      * @see <a href="https://tools.ietf.org/html/rfc3230">RFC 3230</a>
252      * @see <a href="https://tools.ietf.org/html/rfc5843">RFC 45843</a>
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         // two characters form the hex value.
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             //is directory
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                 //delete (sub-)directory and all its containing files and dirs 
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             //does not exist
471             return createFile(contents, mcrPath);
472         }
473         //file does already exist
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         //as per https://tools.ietf.org/html/rfc7230#section-3.3
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             //A SecureDirectoryStream may get attributes faster than reading attributes for every path instance
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() //usually faster
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() //directories first, than sort for filename
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 }