Java tutorial
/******************************************************************************* * Copyright (c) 2015, 2016 Sebastian Stenzel and others. * This file is licensed under the terms of the MIT license. * See the LICENSE.txt file for more info. * * Contributors: * Sebastian Stenzel - initial API and implementation *******************************************************************************/ package org.cryptomator.frontend.webdav.jackrabbitservlet; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavMethods; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceFactory; import org.apache.jackrabbit.webdav.DavResourceLocator; import org.apache.jackrabbit.webdav.DavServletRequest; import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.lock.LockManager; import org.cryptomator.filesystem.jackrabbit.FileLocator; import org.cryptomator.filesystem.jackrabbit.FolderLocator; import org.eclipse.jetty.http.HttpHeader; class FilesystemResourceFactory implements DavResourceFactory { private static final String RANGE_BYTE_PREFIX = "bytes="; private static final char RANGE_SET_SEP = ','; private static final char RANGE_SEP = '-'; private final LockManager lockManager; public FilesystemResourceFactory() { this.lockManager = new ExclusiveSharedLockManager(); } @Override public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { if (locator instanceof FileLocator && DavMethods.METHOD_GET.equals(request.getMethod()) && request.getHeader(HttpHeader.RANGE.asString()) != null) { return createFileRange((FileLocator) locator, request.getDavSession(), request, response); } else { return createResource(locator, request.getDavSession()); } } @Override public DavResource createResource(DavResourceLocator locator, DavSession session) { if (locator instanceof FolderLocator) { FolderLocator folder = (FolderLocator) locator; return createFolder(folder, session); } else if (locator instanceof FileLocator) { FileLocator file = (FileLocator) locator; return createFile(file, session); } else { throw new IllegalArgumentException("Unsupported locator type " + locator.getClass().getName()); } } DavFolder createFolder(FolderLocator folder, DavSession session) { return new DavFolder(this, lockManager, session, folder); } DavFile createFile(FileLocator file, DavSession session) { return new DavFile(this, lockManager, session, file); } private DavFile createFileRange(FileLocator file, DavSession session, DavServletRequest request, DavServletResponse response) throws DavException { // 404 for non-existing resources: if (!file.exists()) { throw new DavException(DavServletResponse.SC_NOT_FOUND); } // 200 for "normal" resources, if if-range is not satisified: final String ifRangeHeader = request.getHeader(HttpHeader.IF_RANGE.asString()); if (!isIfRangeHeaderSatisfied(file, ifRangeHeader)) { return createFile(file, session); } final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); try { // 206 for ranged resources: final Pair<String, String> parsedRange = parseSingleByteRange(rangeHeader); response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT); return new DavFileWithRange(this, lockManager, session, file, parsedRange); } catch (NotASingleByteRangeException ex) { return createFile(file, session); } } /** * Processes the given range header field, if it is supported. Only headers containing a single byte range are supported.<br/> * <code> * bytes=100-200<br/> * bytes=-500<br/> * bytes=1000- * </code> * * @return Tuple of lower and upper range. * @throws DavException HTTP statuscode 400 for malformed requests. * @throws NotASingleByteRangeException Indicating a range that is not supported by this server, i.e. range header should be ignored. */ private Pair<String, String> parseSingleByteRange(String rangeHeader) throws DavException, NotASingleByteRangeException { assert rangeHeader != null; if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) { throw new NotASingleByteRangeException(); } final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX); final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP); if (byteRanges.length != 1) { throw new NotASingleByteRangeException(); } final String byteRange = byteRanges[0]; final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP); if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) { throw new DavException(DavServletResponse.SC_BAD_REQUEST, "malformed range header: " + rangeHeader); } return new ImmutablePair<>(bytePos[0], bytePos[1]); } /** * @return <code>true</code> if a partial response should be generated according to an If-Range precondition. */ private boolean isIfRangeHeaderSatisfied(FileLocator file, String ifRangeHeader) throws DavException { if (ifRangeHeader == null) { // no header set -> satisfied implicitly return true; } else { try { Instant expectedTime = Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(ifRangeHeader)); Instant actualTime = file.lastModified(); return expectedTime.compareTo(actualTime) == 0; } catch (DateTimeParseException e) { throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unsupported If-Range header: " + ifRangeHeader); } } } private static class NotASingleByteRangeException extends Exception { } }