2 * Copyright 2007, 2008, 2009 Electronic Business Systems Ltd.
4 * This file is part of GSS.
6 * GSS is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * GSS is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with GSS. If not, see <http://www.gnu.org/licenses/>.
19 package gr.ebs.gss.server.webdav;
21 import static gr.ebs.gss.server.configuration.GSSConfigurationFactory.getConfiguration;
22 import gr.ebs.gss.client.exceptions.DuplicateNameException;
23 import gr.ebs.gss.client.exceptions.GSSIOException;
24 import gr.ebs.gss.client.exceptions.InsufficientPermissionsException;
25 import gr.ebs.gss.client.exceptions.ObjectNotFoundException;
26 import gr.ebs.gss.client.exceptions.QuotaExceededException;
27 import gr.ebs.gss.client.exceptions.RpcException;
28 import gr.ebs.gss.server.domain.User;
29 import gr.ebs.gss.server.domain.dto.AuditInfoDTO;
30 import gr.ebs.gss.server.domain.dto.FileBodyDTO;
31 import gr.ebs.gss.server.domain.dto.FileHeaderDTO;
32 import gr.ebs.gss.server.domain.dto.FolderDTO;
33 import gr.ebs.gss.server.ejb.ExternalAPI;
34 import gr.ebs.gss.server.ejb.TransactionHelper;
36 import java.io.BufferedInputStream;
37 import java.io.ByteArrayInputStream;
38 import java.io.ByteArrayOutputStream;
40 import java.io.FileInputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.InputStreamReader;
44 import java.io.OutputStreamWriter;
45 import java.io.PrintWriter;
46 import java.io.RandomAccessFile;
47 import java.io.Reader;
48 import java.io.StringReader;
49 import java.io.StringWriter;
50 import java.io.UnsupportedEncodingException;
51 import java.io.Writer;
52 import java.net.URLDecoder;
53 import java.security.MessageDigest;
54 import java.security.NoSuchAlgorithmException;
55 import java.text.SimpleDateFormat;
56 import java.util.ArrayList;
57 import java.util.Date;
58 import java.util.Enumeration;
59 import java.util.Hashtable;
60 import java.util.Iterator;
61 import java.util.List;
62 import java.util.Locale;
63 import java.util.Stack;
64 import java.util.StringTokenizer;
65 import java.util.TimeZone;
66 import java.util.Vector;
67 import java.util.concurrent.Callable;
69 import javax.naming.Context;
70 import javax.naming.InitialContext;
71 import javax.naming.NamingException;
72 import javax.rmi.PortableRemoteObject;
73 import javax.servlet.ServletContext;
74 import javax.servlet.ServletException;
75 import javax.servlet.ServletOutputStream;
76 import javax.servlet.UnavailableException;
77 import javax.servlet.http.HttpServlet;
78 import javax.servlet.http.HttpServletRequest;
79 import javax.servlet.http.HttpServletResponse;
80 import javax.xml.parsers.DocumentBuilder;
81 import javax.xml.parsers.DocumentBuilderFactory;
82 import javax.xml.parsers.ParserConfigurationException;
84 import org.apache.commons.httpclient.HttpStatus;
85 import org.apache.commons.logging.Log;
86 import org.apache.commons.logging.LogFactory;
87 import org.w3c.dom.Document;
88 import org.w3c.dom.Element;
89 import org.w3c.dom.Node;
90 import org.w3c.dom.NodeList;
91 import org.xml.sax.EntityResolver;
92 import org.xml.sax.InputSource;
93 import org.xml.sax.SAXException;
96 * The implementation of the WebDAV service.
100 public class Webdav extends HttpServlet {
103 * The request attribute containing the user who owns the requested
106 protected static final String OWNER_ATTRIBUTE = "owner";
109 * The request attribute containing the user making the request.
111 protected static final String USER_ATTRIBUTE = "user";
116 private static Log logger = LogFactory.getLog(Webdav.class);
121 protected static final String METHOD_GET = "GET";
126 protected static final String METHOD_POST = "POST";
131 protected static final String METHOD_PUT = "PUT";
136 protected static final String METHOD_DELETE = "DELETE";
141 protected static final String METHOD_HEAD = "HEAD";
146 private static final String METHOD_OPTIONS = "OPTIONS";
151 private static final String METHOD_PROPFIND = "PROPFIND";
156 private static final String METHOD_PROPPATCH = "PROPPATCH";
161 private static final String METHOD_MKCOL = "MKCOL";
166 private static final String METHOD_COPY = "COPY";
171 private static final String METHOD_MOVE = "MOVE";
176 private static final String METHOD_LOCK = "LOCK";
181 private static final String METHOD_UNLOCK = "UNLOCK";
184 * Default depth is infinite.
186 static final int INFINITY = 3; // To limit tree browsing a bit
189 * PROPFIND - Specify a property mask.
191 private static final int FIND_BY_PROPERTY = 0;
194 * PROPFIND - Display all properties.
196 private static final int FIND_ALL_PROP = 1;
199 * PROPFIND - Return property names.
201 private static final int FIND_PROPERTY_NAMES = 2;
206 private static final String DEFAULT_NAMESPACE = "DAV:";
211 private static final int LOCK_CREATION = 0;
216 private static final int LOCK_REFRESH = 1;
219 * Default lock timeout value.
221 private static final int DEFAULT_TIMEOUT = 3600;
224 * Maximum lock timeout.
226 private static final int MAX_TIMEOUT = 604800;
229 * Size of file transfer buffer in bytes.
231 private static final int BUFFER_SIZE = 4096;
234 * The output buffer size to use when serving resources.
236 protected int output = 2048;
239 * The input buffer size to use when serving resources.
241 private int input = 2048;
244 * MIME multipart separation string
246 protected static final String mimeSeparation = "GSS_MIME_BOUNDARY";
249 * Simple date format for the creation date ISO representation (partial).
251 private static final SimpleDateFormat creationDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
256 private static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
259 * Array containing the safe characters set.
261 private static URLEncoder urlEncoder;
264 * File encoding to be used when reading static files. If none is specified
265 * the platform default is used.
267 private String fileEncoding = null;
270 * The style sheet for displaying the directory listings.
272 private static final String GSS_CSS = "H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} " + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} " + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} " + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} " + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} " + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}" + "A {color : black;}" + "A.name {color : black;}" + "HR {color : #525D76;}";
275 * Secret information used to generate reasonably secure lock ids.
277 private String secret = "gss-webdav";
283 protected static ArrayList FULL = new ArrayList();
286 * MD5 message digest provider.
288 protected static MessageDigest md5Helper;
291 * The MD5 helper object for this class.
293 protected static final MD5Encoder md5Encoder = new MD5Encoder();
296 * GMT timezone - all HTTP dates are on GMT
299 creationDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
300 urlEncoder = new URLEncoder();
301 urlEncoder.addSafeCharacter('-');
302 urlEncoder.addSafeCharacter('_');
303 urlEncoder.addSafeCharacter('.');
304 urlEncoder.addSafeCharacter('*');
305 urlEncoder.addSafeCharacter('/');
309 public void init() throws ServletException {
310 if (getServletConfig().getInitParameter("input") != null)
311 input = Integer.parseInt(getServletConfig().getInitParameter("input"));
313 if (getServletConfig().getInitParameter("output") != null)
314 output = Integer.parseInt(getServletConfig().getInitParameter("output"));
316 fileEncoding = getServletConfig().getInitParameter("fileEncoding");
318 // Sanity check on the specified buffer sizes
323 if (logger.isDebugEnabled())
324 logger.debug("Input buffer size=" + input + ", output buffer size=" + output);
326 if (getServletConfig().getInitParameter("secret") != null)
327 secret = getServletConfig().getInitParameter("secret");
329 // Load the MD5 helper used to calculate signatures.
331 md5Helper = MessageDigest.getInstance("MD5");
332 } catch (NoSuchAlgorithmException e) {
333 throw new UnavailableException("No MD5");
338 * A helper method that retrieves a reference to the ExternalAPI bean and
339 * stores it for future use.
341 * @return an ExternalAPI instance
342 * @throws RpcException in case an error occurs
344 protected ExternalAPI getService() throws RpcException {
346 final Context ctx = new InitialContext();
347 final Object ref = ctx.lookup(getConfiguration().getString("externalApiPath"));
348 return (ExternalAPI) PortableRemoteObject.narrow(ref, ExternalAPI.class);
349 } catch (final NamingException e) {
350 logger.error("Unable to retrieve the ExternalAPI EJB", e);
351 throw new RpcException("An error occurred while contacting the naming service");
355 private void updateAccounting(final User user, final Date date, final long bandwidthDiff) {
357 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
359 public Void call() throws Exception {
360 getService().updateAccounting(user, date, bandwidthDiff);
364 } catch (RuntimeException e) {
366 } catch (Exception e) {
367 // updateAccounting() doesn't throw any checked exceptions
373 public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
374 String method = request.getMethod();
376 if (logger.isDebugEnabled()) {
377 String path = request.getPathInfo();
379 path = request.getServletPath();
380 if (path == null || path.equals(""))
382 logger.debug("[" + method + "] " + path);
387 if (request.getUserPrincipal() != null) { // Let unauthenticated
388 // OPTIONS go through;
389 // all others will be
391 // authentication anyway
392 // before we get here.
393 user = getService().findUser(request.getUserPrincipal().getName());
395 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
399 request.setAttribute(USER_ATTRIBUTE, user);
400 request.setAttribute(OWNER_ATTRIBUTE, user);
401 } catch (RpcException e) {
402 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
405 if (method.equals(METHOD_GET))
406 doGet(request, response);
407 else if (method.equals(METHOD_POST))
408 doPost(request, response);
409 else if (method.equals(METHOD_PUT))
410 doPut(request, response);
411 else if (method.equals(METHOD_DELETE))
412 doDelete(request, response);
413 else if (method.equals(METHOD_HEAD))
414 doHead(request, response);
415 else if (method.equals(METHOD_PROPFIND))
416 doPropfind(request, response);
417 else if (method.equals(METHOD_PROPPATCH))
418 doProppatch(request, response);
419 else if (method.equals(METHOD_MKCOL))
420 doMkcol(request, response);
421 else if (method.equals(METHOD_COPY))
422 doCopy(request, response);
423 else if (method.equals(METHOD_MOVE))
424 doMove(request, response);
425 else if (method.equals(METHOD_LOCK))
426 doLock(request, response);
427 else if (method.equals(METHOD_UNLOCK))
428 doUnlock(request, response);
429 else if (method.equals(METHOD_OPTIONS))
430 doOptions(request, response);
432 // DefaultServlet processing for TRACE, etc.
433 super.service(request, response);
437 protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws IOException {
438 resp.addHeader("DAV", "1,2");
439 StringBuffer methodsAllowed = new StringBuffer();
441 methodsAllowed = determineMethodsAllowed(req);
442 } catch (RpcException e) {
443 resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
446 resp.addHeader("Allow", methodsAllowed.toString());
447 resp.addHeader("MS-Author-Via", "DAV");
451 * Implement the PROPFIND method.
453 * @param req the HTTP request
454 * @param resp the HTTP response
455 * @throws ServletException
456 * @throws IOException
458 private void doPropfind(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
459 String path = getRelativePath(req);
460 if (path.endsWith("/") && !path.equals("/"))
461 path = path.substring(0, path.length() - 1);
463 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
464 resp.sendError(WebdavStatus.SC_FORBIDDEN);
468 // Properties which are to be displayed.
469 Vector<String> properties = null;
471 int depth = INFINITY;
473 int type = FIND_ALL_PROP;
475 String depthStr = req.getHeader("Depth");
477 if (depthStr == null)
479 else if (depthStr.equals("0"))
481 else if (depthStr.equals("1"))
483 else if (depthStr.equals("infinity"))
486 Node propNode = null;
488 if (req.getInputStream().available() > 0) {
489 DocumentBuilder documentBuilder = getDocumentBuilder();
492 Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
494 // Get the root element of the document
495 Element rootElement = document.getDocumentElement();
496 NodeList childList = rootElement.getChildNodes();
498 for (int i = 0; i < childList.getLength(); i++) {
499 Node currentNode = childList.item(i);
500 switch (currentNode.getNodeType()) {
503 case Node.ELEMENT_NODE:
504 if (currentNode.getNodeName().endsWith("prop")) {
505 type = FIND_BY_PROPERTY;
506 propNode = currentNode;
508 if (currentNode.getNodeName().endsWith("propname"))
509 type = FIND_PROPERTY_NAMES;
510 if (currentNode.getNodeName().endsWith("allprop"))
511 type = FIND_ALL_PROP;
515 } catch (SAXException e) {
516 // Something went wrong - use the defaults.
517 if (logger.isDebugEnabled())
518 logger.debug(e.getMessage());
519 } catch (IOException e) {
520 // Something went wrong - use the defaults.
521 if (logger.isDebugEnabled())
522 logger.debug(e.getMessage());
526 if (type == FIND_BY_PROPERTY) {
527 properties = new Vector<String>();
528 NodeList childList = propNode.getChildNodes();
530 for (int i = 0; i < childList.getLength(); i++) {
531 Node currentNode = childList.item(i);
532 switch (currentNode.getNodeType()) {
535 case Node.ELEMENT_NODE:
536 String nodeName = currentNode.getNodeName();
537 String propertyName = null;
538 if (nodeName.indexOf(':') != -1)
539 propertyName = nodeName.substring(nodeName.indexOf(':') + 1);
541 propertyName = nodeName;
542 // href is a live property which is handled differently
543 properties.addElement(propertyName);
548 User user = getUser(req);
549 boolean exists = true;
550 Object object = null;
552 object = getService().getResourceAtPath(user.getId(), path, true);
553 } catch (ObjectNotFoundException e) {
555 } catch (RpcException e) {
556 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
560 resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
563 resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
564 resp.setContentType("text/xml; charset=UTF-8");
565 // Create multistatus object
566 XMLWriter generatedXML = new XMLWriter(resp.getWriter());
567 generatedXML.writeXMLHeader();
568 generatedXML.writeElement(null, "multistatus" + generateNamespaceDeclarations(), XMLWriter.OPENING);
570 parseProperties(req, generatedXML, path, type, properties, object);
572 // The stack always contains the object of the current level
573 Stack<String> stack = new Stack<String>();
576 // Stack of the objects one level below
577 Stack<String> stackBelow = new Stack<String>();
578 while (!stack.isEmpty() && depth >= 0) {
579 String currentPath = stack.pop();
581 object = getService().getResourceAtPath(user.getId(), currentPath, true);
582 } catch (ObjectNotFoundException e) {
584 } catch (RpcException e) {
585 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
588 parseProperties(req, generatedXML, currentPath, type, properties, object);
589 if (object instanceof FolderDTO && depth > 0) {
590 FolderDTO folder = (FolderDTO) object;
591 // Retrieve the subfolders.
592 List subfolders = folder.getSubfolders();
593 Iterator iter = subfolders.iterator();
594 while (iter.hasNext()) {
595 FolderDTO f = (FolderDTO) iter.next();
596 String newPath = currentPath;
597 if (!newPath.endsWith("/"))
599 newPath += f.getName();
600 stackBelow.push(newPath);
602 // Retrieve the files.
603 List<FileHeaderDTO> files;
605 files = getService().getFiles(user.getId(), folder.getId(), true);
606 } catch (ObjectNotFoundException e) {
607 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
609 } catch (InsufficientPermissionsException e) {
610 resp.sendError(HttpServletResponse.SC_FORBIDDEN, path);
612 } catch (RpcException e) {
613 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
616 for (FileHeaderDTO file : files) {
617 String newPath = currentPath;
618 if (!newPath.endsWith("/"))
620 newPath += file.getName();
621 stackBelow.push(newPath);
624 if (stack.isEmpty()) {
627 stackBelow = new Stack<String>();
629 generatedXML.sendData();
632 generatedXML.writeElement(null, "multistatus", XMLWriter.CLOSING);
633 generatedXML.sendData();
639 * @param req the HTTP request
640 * @param resp the HTTP response
641 * @throws IOException if an error occurs while sending the response
643 private void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
645 resp.sendError(WebdavStatus.SC_LOCKED);
648 resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
652 * @see javax.servlet.http.HttpServlet#doDelete(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
655 protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
657 resp.sendError(WebdavStatus.SC_LOCKED);
660 deleteResource(req, resp);
664 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
665 // Serve the requested resource, including the data content
667 serveResource(req, resp, true);
668 } catch (ObjectNotFoundException e) {
669 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
671 } catch (InsufficientPermissionsException e) {
672 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
674 } catch (RpcException e) {
675 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
681 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
682 resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
686 protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
688 resp.sendError(WebdavStatus.SC_LOCKED);
692 final User user = getUser(req);
693 String path = getRelativePath(req);
694 boolean exists = true;
695 Object resource = null;
696 FileHeaderDTO file = null;
698 resource = getService().getResourceAtPath(user.getId(), path, true);
699 } catch (ObjectNotFoundException e) {
701 } catch (RpcException e) {
702 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
707 if (resource instanceof FileHeaderDTO)
708 file = (FileHeaderDTO) resource;
710 resp.sendError(HttpServletResponse.SC_CONFLICT);
713 boolean result = true;
715 // Temporary content file used to support partial PUT.
716 File contentFile = null;
718 Range range = parseContentRange(req, resp);
720 InputStream resourceInputStream = null;
722 // Append data specified in ranges to existing content for this
723 // resource - create a temporary file on the local filesystem to
724 // perform this operation.
725 // Assume just one range is specified for now
728 contentFile = executePartialPut(req, range, path);
729 } catch (RpcException e) {
730 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
732 } catch (ObjectNotFoundException e) {
733 resp.sendError(HttpServletResponse.SC_CONFLICT);
735 } catch (InsufficientPermissionsException e) {
736 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
739 resourceInputStream = new FileInputStream(contentFile);
741 resourceInputStream = req.getInputStream();
744 Object parent = getService().getResourceAtPath(user.getId(), getParentPath(path), true);
745 if (!(parent instanceof FolderDTO)) {
746 resp.sendError(HttpServletResponse.SC_CONFLICT);
749 final FolderDTO folder = (FolderDTO) parent;
750 final String name = getLastElement(path);
751 final String mimeType = getServletContext().getMimeType(name);
752 File uploadedFile = null;
754 uploadedFile = getService().uploadFile(resourceInputStream, user.getId());
755 } catch (IOException ex) {
756 throw new GSSIOException(ex, false);
758 // FIXME: Add attributes
759 FileHeaderDTO fileDTO = null;
760 final FileHeaderDTO f = file;
761 final File uf = uploadedFile;
763 fileDTO = new TransactionHelper<FileHeaderDTO>().tryExecute(new Callable<FileHeaderDTO>() {
765 public FileHeaderDTO call() throws Exception {
766 return getService().updateFileContents(user.getId(), f.getId(), mimeType, uf.length(), uf.getAbsolutePath());
770 fileDTO = new TransactionHelper<FileHeaderDTO>().tryExecute(new Callable<FileHeaderDTO>() {
772 public FileHeaderDTO call() throws Exception {
773 return getService().createFile(user.getId(), folder.getId(), name, mimeType, uf.length(), uf.getAbsolutePath());
776 updateAccounting(user, new Date(), fileDTO.getFileSize());
777 } catch (ObjectNotFoundException e) {
779 } catch (InsufficientPermissionsException e) {
780 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
782 } catch (QuotaExceededException e) {
783 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
785 } catch (GSSIOException e) {
786 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
788 } catch (RpcException e) {
789 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
791 } catch (DuplicateNameException e) {
792 resp.sendError(HttpServletResponse.SC_CONFLICT);
794 } catch (Exception e) {
795 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
801 resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
803 resp.setStatus(HttpServletResponse.SC_CREATED);
805 resp.sendError(HttpServletResponse.SC_CONFLICT);
810 protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
811 // Serve the requested resource, without the data content
813 serveResource(req, resp, false);
814 } catch (ObjectNotFoundException e) {
815 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
817 } catch (InsufficientPermissionsException e) {
818 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
820 } catch (RpcException e) {
821 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
829 * @param req the HTTP request
830 * @param resp the HTTP response
831 * @throws IOException if an error occurs while sending the response
833 private void doUnlock(@SuppressWarnings("unused") HttpServletRequest req, HttpServletResponse resp) throws IOException {
834 resp.setStatus(WebdavStatus.SC_NO_CONTENT);
840 * @param req the HTTP request
841 * @param resp the HTTP response
842 * @throws IOException if an error occurs while sending the response
843 * @throws ServletException
845 private void doLock(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
846 LockInfo lock = new LockInfo();
847 // Parsing lock request
849 // Parsing depth header
850 String depthStr = req.getHeader("Depth");
851 if (depthStr == null)
852 lock.depth = INFINITY;
853 else if (depthStr.equals("0"))
856 lock.depth = INFINITY;
858 // Parsing timeout header
859 int lockDuration = DEFAULT_TIMEOUT;
860 String lockDurationStr = req.getHeader("Timeout");
861 if (lockDurationStr == null)
862 lockDuration = DEFAULT_TIMEOUT;
864 int commaPos = lockDurationStr.indexOf(",");
865 // If multiple timeouts, just use the first
867 lockDurationStr = lockDurationStr.substring(0, commaPos);
868 if (lockDurationStr.startsWith("Second-"))
869 lockDuration = new Integer(lockDurationStr.substring(7)).intValue();
870 else if (lockDurationStr.equalsIgnoreCase("infinity"))
871 lockDuration = MAX_TIMEOUT;
874 lockDuration = new Integer(lockDurationStr).intValue();
875 } catch (NumberFormatException e) {
876 lockDuration = MAX_TIMEOUT;
878 if (lockDuration == 0)
879 lockDuration = DEFAULT_TIMEOUT;
880 if (lockDuration > MAX_TIMEOUT)
881 lockDuration = MAX_TIMEOUT;
883 lock.expiresAt = System.currentTimeMillis() + lockDuration * 1000;
885 int lockRequestType = LOCK_CREATION;
886 Node lockInfoNode = null;
887 DocumentBuilder documentBuilder = getDocumentBuilder();
890 Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
891 // Get the root element of the document
892 Element rootElement = document.getDocumentElement();
893 lockInfoNode = rootElement;
894 } catch (IOException e) {
895 lockRequestType = LOCK_REFRESH;
896 } catch (SAXException e) {
897 lockRequestType = LOCK_REFRESH;
900 if (lockInfoNode != null) {
901 // Reading lock information
902 NodeList childList = lockInfoNode.getChildNodes();
903 StringWriter strWriter = null;
904 DOMWriter domWriter = null;
906 Node lockScopeNode = null;
907 Node lockTypeNode = null;
908 Node lockOwnerNode = null;
910 for (int i = 0; i < childList.getLength(); i++) {
911 Node currentNode = childList.item(i);
912 switch (currentNode.getNodeType()) {
915 case Node.ELEMENT_NODE:
916 String nodeName = currentNode.getNodeName();
917 if (nodeName.endsWith("lockscope"))
918 lockScopeNode = currentNode;
919 if (nodeName.endsWith("locktype"))
920 lockTypeNode = currentNode;
921 if (nodeName.endsWith("owner"))
922 lockOwnerNode = currentNode;
927 if (lockScopeNode != null) {
928 childList = lockScopeNode.getChildNodes();
929 for (int i = 0; i < childList.getLength(); i++) {
930 Node currentNode = childList.item(i);
931 switch (currentNode.getNodeType()) {
934 case Node.ELEMENT_NODE:
935 String tempScope = currentNode.getNodeName();
936 if (tempScope.indexOf(':') != -1)
937 lock.scope = tempScope.substring(tempScope.indexOf(':') + 1);
939 lock.scope = tempScope;
943 if (lock.scope == null)
945 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
948 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
950 if (lockTypeNode != null) {
951 childList = lockTypeNode.getChildNodes();
952 for (int i = 0; i < childList.getLength(); i++) {
953 Node currentNode = childList.item(i);
954 switch (currentNode.getNodeType()) {
957 case Node.ELEMENT_NODE:
958 String tempType = currentNode.getNodeName();
959 if (tempType.indexOf(':') != -1)
960 lock.type = tempType.substring(tempType.indexOf(':') + 1);
962 lock.type = tempType;
967 if (lock.type == null)
969 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
972 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
974 if (lockOwnerNode != null) {
975 childList = lockOwnerNode.getChildNodes();
976 for (int i = 0; i < childList.getLength(); i++) {
977 Node currentNode = childList.item(i);
978 switch (currentNode.getNodeType()) {
980 lock.owner += currentNode.getNodeValue();
982 case Node.ELEMENT_NODE:
983 strWriter = new StringWriter();
984 domWriter = new DOMWriter(strWriter, true);
985 domWriter.setQualifiedNames(false);
986 domWriter.print(currentNode);
987 lock.owner += strWriter.toString();
992 if (lock.owner == null)
994 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
996 lock.owner = new String();
999 String path = getRelativePath(req);
1001 User user = getUser(req);
1002 boolean exists = true;
1003 Object object = null;
1005 object = getService().getResourceAtPath(user.getId(), path, true);
1006 } catch (ObjectNotFoundException e) {
1008 } catch (RpcException e) {
1009 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1013 if (lockRequestType == LOCK_CREATION) {
1014 // Generating lock id
1015 String lockTokenStr = req.getServletPath() + "-" + lock.type + "-" + lock.scope + "-" + req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.tokens + "-" + lock.expiresAt + "-" + System.currentTimeMillis() + "-" + secret;
1016 String lockToken = md5Encoder.encode(md5Helper.digest(lockTokenStr.getBytes()));
1018 if (exists && object instanceof FolderDTO && lock.depth == INFINITY)
1019 // Locking a collection (and all its member resources)
1020 lock.tokens.addElement(lockToken);
1022 // Locking a single resource
1023 lock.tokens.addElement(lockToken);
1024 // Add the Lock-Token header as by RFC 2518 8.10.1
1025 // - only do this for newly created locks
1026 resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">");
1031 if (lockRequestType == LOCK_REFRESH) {
1035 // Set the status, then generate the XML response containing
1036 // the lock information.
1037 XMLWriter generatedXML = new XMLWriter();
1038 generatedXML.writeXMLHeader();
1039 generatedXML.writeElement(null, "prop" + generateNamespaceDeclarations(), XMLWriter.OPENING);
1040 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.OPENING);
1041 lock.toXML(generatedXML);
1042 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.CLOSING);
1043 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1045 resp.setStatus(WebdavStatus.SC_OK);
1046 resp.setContentType("text/xml; charset=UTF-8");
1047 Writer writer = resp.getWriter();
1048 writer.write(generatedXML.toString());
1055 * @param req the HTTP request
1056 * @param resp the HTTP response
1057 * @throws IOException if an error occurs while sending the response
1058 * @throws ServletException
1060 private void doMove(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1061 if (isLocked(req)) {
1062 resp.sendError(WebdavStatus.SC_LOCKED);
1066 String path = getRelativePath(req);
1068 if (copyResource(req, resp))
1069 deleteResource(path, req, resp, false);
1075 * @param req the HTTP request
1076 * @param resp the HTTP response
1077 * @throws IOException if an error occurs while sending the response
1078 * @throws ServletException
1080 private void doCopy(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1081 copyResource(req, resp);
1087 * @param req the HTTP request
1088 * @param resp the HTTP response
1089 * @throws IOException if an error occurs while sending the response
1090 * @throws ServletException
1092 private void doMkcol(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1093 if (isLocked(req)) {
1094 resp.sendError(WebdavStatus.SC_LOCKED);
1097 final String path = getRelativePath(req);
1098 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
1099 resp.sendError(WebdavStatus.SC_FORBIDDEN);
1103 final User user = getUser(req);
1104 boolean exists = true;
1106 getService().getResourceAtPath(user.getId(), path, true);
1107 } catch (ObjectNotFoundException e) {
1109 } catch (RpcException e) {
1110 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1114 // Can't create a collection if a resource already exists at the given
1117 // Get allowed methods.
1118 StringBuffer methodsAllowed;
1120 methodsAllowed = determineMethodsAllowed(req);
1121 } catch (RpcException e) {
1122 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1125 resp.addHeader("Allow", methodsAllowed.toString());
1126 resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
1130 if (req.getInputStream().available() > 0) {
1131 DocumentBuilder documentBuilder = getDocumentBuilder();
1133 Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
1134 // TODO : Process this request body
1135 resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED);
1137 } catch (SAXException saxe) {
1138 // Parse error - assume invalid content
1139 resp.sendError(WebdavStatus.SC_BAD_REQUEST);
1146 parent = getService().getResourceAtPath(user.getId(), getParentPath(path), true);
1147 } catch (ObjectNotFoundException e1) {
1148 resp.sendError(WebdavStatus.SC_CONFLICT, WebdavStatus.getStatusText(WebdavStatus.SC_CONFLICT));
1150 } catch (RpcException e1) {
1151 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1155 if (parent instanceof FolderDTO) {
1156 final FolderDTO folder = (FolderDTO) parent;
1157 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1159 public Void call() throws Exception {
1160 getService().createFolder(user.getId(), folder.getId(), getLastElement(path));
1165 resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1168 } catch (DuplicateNameException e) {
1169 // XXX If the existing name is a folder we should be returning
1170 // SC_METHOD_NOT_ALLOWED, or even better, just do the createFolder
1171 // without checking first and then deal with the exceptions.
1172 resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1174 } catch (InsufficientPermissionsException e) {
1175 resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1177 } catch (ObjectNotFoundException e) {
1178 resp.sendError(WebdavStatus.SC_CONFLICT, WebdavStatus.getStatusText(WebdavStatus.SC_CONFLICT));
1180 } catch (RpcException e) {
1181 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1183 } catch (Exception e) {
1184 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1187 resp.setStatus(WebdavStatus.SC_CREATED);
1191 * For a provided path, remove the last element and return the rest, that is
1192 * the path of the parent folder.
1194 * @param path the specified path
1195 * @return the path of the parent folder
1196 * @throws ObjectNotFoundException if the provided string contains no path
1199 protected String getParentPath(String path) throws ObjectNotFoundException {
1200 int lastDelimiter = path.lastIndexOf('/');
1201 if (lastDelimiter == 0)
1203 if (lastDelimiter == -1)
1205 throw new ObjectNotFoundException("There is no parent in the path: " + path);
1206 else if (lastDelimiter < path.length() - 1)
1207 // Return the part before the delimiter.
1208 return path.substring(0, lastDelimiter);
1210 // Remove the trailing delimiter and then recurse.
1211 String strippedTrail = path.substring(0, lastDelimiter);
1212 return getParentPath(strippedTrail);
1217 * Get the last element in a path that denotes the file or folder name.
1219 * @param path the provided path
1220 * @return the last element in the path
1222 protected String getLastElement(String path) {
1223 int lastDelimiter = path.lastIndexOf('/');
1224 if (lastDelimiter == -1)
1227 else if (lastDelimiter < path.length() - 1)
1228 // Return the part after the delimiter.
1229 return path.substring(lastDelimiter + 1);
1231 // Remove the trailing delimiter and then recurse.
1232 String strippedTrail = path.substring(0, lastDelimiter);
1233 return getLastElement(strippedTrail);
1238 * Only use the PathInfo for determining the requested path. If the
1239 * ServletPath is non-null, it will be because the WebDAV servlet has been
1240 * mapped to a URL other than /* to configure editing at different URL than
1243 * @param request the servlet request we are processing
1244 * @return the relative path
1245 * @throws UnsupportedEncodingException
1247 protected String getRelativePath(HttpServletRequest request) {
1248 // Remove the servlet path from the request URI.
1249 String p = request.getRequestURI();
1250 String servletPath = request.getContextPath() + request.getServletPath();
1251 String result = p.substring(servletPath.length());
1253 result = URLDecoder.decode(result, "UTF-8");
1254 } catch (UnsupportedEncodingException e) {
1256 if (result == null || result.equals(""))
1263 * Return JAXP document builder instance.
1265 * @return the DocumentBuilder
1266 * @throws ServletException
1268 private DocumentBuilder getDocumentBuilder() throws ServletException {
1269 DocumentBuilder documentBuilder = null;
1270 DocumentBuilderFactory documentBuilderFactory = null;
1272 documentBuilderFactory = DocumentBuilderFactory.newInstance();
1273 documentBuilderFactory.setNamespaceAware(true);
1274 documentBuilderFactory.setExpandEntityReferences(false);
1275 documentBuilder = documentBuilderFactory.newDocumentBuilder();
1276 documentBuilder.setEntityResolver(new WebdavResolver(getServletContext()));
1277 } catch (ParserConfigurationException e) {
1278 throw new ServletException("Error while creating a document builder");
1280 return documentBuilder;
1284 * Generate the namespace declarations.
1286 * @return the namespace declarations
1288 private String generateNamespaceDeclarations() {
1289 return " xmlns=\"" + DEFAULT_NAMESPACE + "\"";
1293 * Propfind helper method. Dispays the properties of a lock-null resource.
1295 * @param req the HTTP request
1296 * @param resources Resources object associated with this context
1297 * @param generatedXML XML response to the Propfind request
1298 * @param path Path of the current resource
1299 * @param type Propfind type
1300 * @param propertiesVector If the propfind type is find properties by name,
1301 * then this Vector contains those properties
1303 @SuppressWarnings("unused")
1304 private void parseLockNullProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, Vector propertiesVector) {
1309 * Propfind helper method.
1311 * @param req The servlet request
1312 * @param resources Resources object associated with this context
1313 * @param generatedXML XML response to the Propfind request
1314 * @param path Path of the current resource
1315 * @param type Propfind type
1316 * @param propertiesVector If the propfind type is find properties by name,
1317 * then this Vector contains those properties
1318 * @param resource the resource object
1320 private void parseProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, Vector<String> propertiesVector, Object resource) {
1322 // Exclude any resource in the /WEB-INF and /META-INF subdirectories
1323 // (the "toUpperCase()" avoids problems on Windows systems)
1324 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF"))
1327 FolderDTO folder = null;
1328 FileHeaderDTO file = null;
1329 if (resource instanceof FolderDTO)
1330 folder = (FolderDTO) resource;
1332 file = (FileHeaderDTO) resource;
1333 // Retrieve the creation date.
1336 creation = folder.getAuditInfo().getCreationDate().getTime();
1338 creation = file.getAuditInfo().getCreationDate().getTime();
1339 // Retrieve the modification date.
1340 long modification = 0;
1342 modification = folder.getAuditInfo().getCreationDate().getTime();
1344 modification = file.getAuditInfo().getCreationDate().getTime();
1346 generatedXML.writeElement(null, "response", XMLWriter.OPENING);
1347 String status = new String("HTTP/1.1 " + WebdavStatus.SC_OK + " " + WebdavStatus.getStatusText(WebdavStatus.SC_OK));
1349 // Generating href element
1350 generatedXML.writeElement(null, "href", XMLWriter.OPENING);
1352 String href = req.getContextPath() + req.getServletPath();
1353 if (href.endsWith("/") && path.startsWith("/"))
1354 href += path.substring(1);
1357 if (folder != null && !href.endsWith("/"))
1360 generatedXML.writeText(rewriteUrl(href));
1362 generatedXML.writeElement(null, "href", XMLWriter.CLOSING);
1364 String resourceName = path;
1365 int lastSlash = path.lastIndexOf('/');
1366 if (lastSlash != -1)
1367 resourceName = resourceName.substring(lastSlash + 1);
1373 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1374 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1376 generatedXML.writeProperty(null, "creationdate", getISOCreationDate(creation));
1377 generatedXML.writeElement(null, "displayname", XMLWriter.OPENING);
1378 generatedXML.writeData(resourceName);
1379 generatedXML.writeElement(null, "displayname", XMLWriter.CLOSING);
1381 generatedXML.writeProperty(null, "getlastmodified", FastHttpDateFormat.formatDate(modification, null));
1382 generatedXML.writeProperty(null, "getcontentlength", String.valueOf(file.getFileSize()));
1383 String contentType = file.getMimeType();
1384 if (contentType != null)
1385 generatedXML.writeProperty(null, "getcontenttype", contentType);
1386 generatedXML.writeProperty(null, "getetag", getETag(file, null));
1387 generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1389 generatedXML.writeElement(null, "resourcetype", XMLWriter.OPENING);
1390 generatedXML.writeElement(null, "collection", XMLWriter.NO_CONTENT);
1391 generatedXML.writeElement(null, "resourcetype", XMLWriter.CLOSING);
1394 generatedXML.writeProperty(null, "source", "");
1396 String supportedLocks = "<lockentry>" + "<lockscope><exclusive/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>" + "<lockentry>" + "<lockscope><shared/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>";
1397 generatedXML.writeElement(null, "supportedlock", XMLWriter.OPENING);
1398 generatedXML.writeText(supportedLocks);
1399 generatedXML.writeElement(null, "supportedlock", XMLWriter.CLOSING);
1401 generateLockDiscovery(path, generatedXML);
1403 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1404 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1405 generatedXML.writeText(status);
1406 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1407 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1411 case FIND_PROPERTY_NAMES:
1413 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1414 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1416 generatedXML.writeElement(null, "creationdate", XMLWriter.NO_CONTENT);
1417 generatedXML.writeElement(null, "displayname", XMLWriter.NO_CONTENT);
1419 generatedXML.writeElement(null, "getcontentlanguage", XMLWriter.NO_CONTENT);
1420 generatedXML.writeElement(null, "getcontentlength", XMLWriter.NO_CONTENT);
1421 generatedXML.writeElement(null, "getcontenttype", XMLWriter.NO_CONTENT);
1422 generatedXML.writeElement(null, "getetag", XMLWriter.NO_CONTENT);
1423 generatedXML.writeElement(null, "getlastmodified", XMLWriter.NO_CONTENT);
1425 generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1426 generatedXML.writeElement(null, "source", XMLWriter.NO_CONTENT);
1427 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.NO_CONTENT);
1429 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1430 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1431 generatedXML.writeText(status);
1432 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1433 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1437 case FIND_BY_PROPERTY:
1439 Vector<String> propertiesNotFound = new Vector<String>();
1441 // Parse the list of properties
1443 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1444 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1446 Enumeration<String> properties = propertiesVector.elements();
1448 while (properties.hasMoreElements()) {
1450 String property = properties.nextElement();
1452 if (property.equals("creationdate"))
1453 generatedXML.writeProperty(null, "creationdate", getISOCreationDate(creation));
1454 else if (property.equals("displayname")) {
1455 generatedXML.writeElement(null, "displayname", XMLWriter.OPENING);
1456 generatedXML.writeData(resourceName);
1457 generatedXML.writeElement(null, "displayname", XMLWriter.CLOSING);
1458 } else if (property.equals("getcontentlanguage")) {
1460 propertiesNotFound.addElement(property);
1462 generatedXML.writeElement(null, "getcontentlanguage", XMLWriter.NO_CONTENT);
1463 } else if (property.equals("getcontentlength")) {
1465 propertiesNotFound.addElement(property);
1467 generatedXML.writeProperty(null, "getcontentlength", String.valueOf(file.getFileSize()));
1468 } else if (property.equals("getcontenttype")) {
1470 propertiesNotFound.addElement(property);
1472 // XXX Once we properly store the MIME type in the
1474 // retrieve it from there.
1475 generatedXML.writeProperty(null, "getcontenttype", getServletContext().getMimeType(file.getName()));
1476 } else if (property.equals("getetag")) {
1478 propertiesNotFound.addElement(property);
1480 generatedXML.writeProperty(null, "getetag", getETag(file, null));
1481 } else if (property.equals("getlastmodified")) {
1483 propertiesNotFound.addElement(property);
1485 generatedXML.writeProperty(null, "getlastmodified", FastHttpDateFormat.formatDate(modification, null));
1486 } else if (property.equals("resourcetype")) {
1487 if (folder != null) {
1488 generatedXML.writeElement(null, "resourcetype", XMLWriter.OPENING);
1489 generatedXML.writeElement(null, "collection", XMLWriter.NO_CONTENT);
1490 generatedXML.writeElement(null, "resourcetype", XMLWriter.CLOSING);
1492 generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1493 } else if (property.equals("source"))
1494 generatedXML.writeProperty(null, "source", "");
1495 else if (property.equals("supportedlock")) {
1496 supportedLocks = "<lockentry>" + "<lockscope><exclusive/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>" + "<lockentry>" + "<lockscope><shared/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>";
1497 generatedXML.writeElement(null, "supportedlock", XMLWriter.OPENING);
1498 generatedXML.writeText(supportedLocks);
1499 generatedXML.writeElement(null, "supportedlock", XMLWriter.CLOSING);
1500 } else if (property.equals("lockdiscovery")) {
1501 if (!generateLockDiscovery(path, generatedXML))
1502 propertiesNotFound.addElement(property);
1504 propertiesNotFound.addElement(property);
1507 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1508 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1509 generatedXML.writeText(status);
1510 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1511 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1513 Enumeration propertiesNotFoundList = propertiesNotFound.elements();
1515 if (propertiesNotFoundList.hasMoreElements()) {
1517 status = new String("HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + " " + WebdavStatus.getStatusText(WebdavStatus.SC_NOT_FOUND));
1519 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1520 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1522 while (propertiesNotFoundList.hasMoreElements())
1523 generatedXML.writeElement(null, (String) propertiesNotFoundList.nextElement(), XMLWriter.NO_CONTENT);
1524 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1525 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1526 generatedXML.writeText(status);
1527 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1528 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1535 generatedXML.writeElement(null, "response", XMLWriter.CLOSING);
1540 * Get the ETag associated with a file.
1542 * @param file the FileHeaderDTO object for this file
1543 * @param oldBody the old version of the file, if requested
1544 * @return a string containing the ETag
1546 protected String getETag(FileHeaderDTO file, FileBodyDTO oldBody) {
1547 if (oldBody == null)
1548 return "\"" + file.getFileSize() + "-" + file.getAuditInfo().getModificationDate().getTime() + "\"";
1549 return "\"" + oldBody.getFileSize() + "-" + oldBody.getAuditInfo().getModificationDate().getTime() + "\"";
1555 * @param path Path which has to be rewritten
1556 * @return the rewritten URL
1558 private String rewriteUrl(String path) {
1559 return urlEncoder.encode(path);
1563 * Print the lock discovery information associated with a path.
1566 * @param generatedXML XML data to which the locks info will be appended
1567 * @return true if at least one lock was displayed
1569 @SuppressWarnings("unused")
1570 private boolean generateLockDiscovery(String path, XMLWriter generatedXML) {
1575 * Get creation date in ISO format.
1577 * @param creationDate
1578 * @return the formatted date
1580 private String getISOCreationDate(long creationDate) {
1581 String dateValue = null;
1582 synchronized (creationDateFormat) {
1583 dateValue = creationDateFormat.format(new Date(creationDate));
1585 StringBuffer creationDateValue = new StringBuffer(dateValue);
1587 int offset = Calendar.getInstance().getTimeZone().getRawOffset()
1588 / 3600000; // FIXME ?
1590 creationDateValue.append("-");
1592 } else if (offset > 0) {
1593 creationDateValue.append("+");
1597 creationDateValue.append("0");
1598 creationDateValue.append(offset + ":00");
1600 creationDateValue.append("Z");
1603 return creationDateValue.toString();
1607 * Determines the methods normally allowed for the resource.
1609 * @param req the HTTP request
1610 * @return a list of the allowed methods
1611 * @throws RpcException if there is an error while communicating with the
1614 private StringBuffer determineMethodsAllowed(HttpServletRequest req) throws RpcException {
1615 StringBuffer methodsAllowed = new StringBuffer();
1616 boolean exists = true;
1617 Object object = null;
1618 User user = getUser(req);
1619 String path = getRelativePath(req);
1620 if (user == null && "/".equals(path))
1621 // Special case: OPTIONS request before authentication
1622 return new StringBuffer("OPTIONS, GET, HEAD, POST, DELETE, TRACE, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, PROPFIND, PUT");
1624 object = getService().getResourceAtPath(user.getId(), path, true);
1625 } catch (ObjectNotFoundException e) {
1630 methodsAllowed.append("OPTIONS, MKCOL, PUT, LOCK");
1631 return methodsAllowed;
1634 methodsAllowed.append("OPTIONS, GET, HEAD, POST, DELETE, TRACE");
1635 methodsAllowed.append(", PROPPATCH, COPY, MOVE, LOCK, UNLOCK");
1636 methodsAllowed.append(", PROPFIND");
1638 if (!(object instanceof FolderDTO))
1639 methodsAllowed.append(", PUT");
1641 return methodsAllowed;
1645 * Check to see if a resource is currently write locked. The method will
1646 * look at the "If" header to make sure the client has given the appropriate
1649 * @param req the HTTP request
1650 * @return boolean true if the resource is locked (and no appropriate lock
1651 * token has been found for at least one of the non-shared locks
1652 * which are present on the resource).
1654 private boolean isLocked(@SuppressWarnings("unused") HttpServletRequest req) {
1659 * Check to see if a resource is currently write locked.
1661 * @param path Path of the resource
1662 * @param ifHeader "If" HTTP header which was included in the request
1663 * @return boolean true if the resource is locked (and no appropriate lock
1664 * token has been found for at least one of the non-shared locks
1665 * which are present on the resource).
1667 private boolean isLocked(@SuppressWarnings("unused") String path, @SuppressWarnings("unused") String ifHeader) {
1672 * Parse the content-range header.
1674 * @param request The servlet request we are processing
1675 * @param response The servlet response we are creating
1677 * @throws IOException
1679 protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response) throws IOException {
1681 // Retrieving the content-range header (if any is specified
1682 String rangeHeader = request.getHeader("Content-Range");
1684 if (rangeHeader == null)
1687 // bytes is the only range unit supported
1688 if (!rangeHeader.startsWith("bytes")) {
1689 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1693 rangeHeader = rangeHeader.substring(6).trim();
1695 int dashPos = rangeHeader.indexOf('-');
1696 int slashPos = rangeHeader.indexOf('/');
1698 if (dashPos == -1) {
1699 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1703 if (slashPos == -1) {
1704 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1708 Range range = new Range();
1711 range.start = Long.parseLong(rangeHeader.substring(0, dashPos));
1712 range.end = Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos));
1713 range.length = Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length()));
1714 } catch (NumberFormatException e) {
1715 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1719 if (!range.validate()) {
1720 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1729 * Handle a partial PUT. New content specified in request is appended to
1730 * existing content in oldRevisionContent (if present). This code does not
1731 * support simultaneous partial updates to the same resource.
1737 * @throws IOException
1738 * @throws RpcException
1739 * @throws InsufficientPermissionsException
1740 * @throws ObjectNotFoundException
1742 protected File executePartialPut(HttpServletRequest req, Range range, String path) throws IOException, RpcException, ObjectNotFoundException, InsufficientPermissionsException {
1743 // Append data specified in ranges to existing content for this
1744 // resource - create a temporary file on the local file system to
1745 // perform this operation.
1746 File tempDir = (File) getServletContext().getAttribute("javax.servlet.context.tempdir");
1747 // Convert all '/' characters to '.' in resourcePath
1748 String convertedResourcePath = path.replace('/', '.');
1749 File contentFile = new File(tempDir, convertedResourcePath);
1750 if (contentFile.createNewFile())
1751 // Clean up contentFile when Tomcat is terminated.
1752 contentFile.deleteOnExit();
1754 RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw");
1756 User user = getUser(req);
1757 User owner = getOwner(req);
1758 FileHeaderDTO oldResource = null;
1760 Object obj = getService().getResourceAtPath(owner.getId(), path, true);
1761 if (obj instanceof FileHeaderDTO)
1762 oldResource = (FileHeaderDTO) obj;
1763 } catch (ObjectNotFoundException e) {
1767 // Copy data in oldRevisionContent to contentFile
1768 if (oldResource != null) {
1769 InputStream contents = getService().getFileContents(user.getId(), oldResource.getId());
1770 BufferedInputStream bufOldRevStream = new BufferedInputStream(contents, BUFFER_SIZE);
1773 byte[] copyBuffer = new byte[BUFFER_SIZE];
1774 while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1)
1775 randAccessContentFile.write(copyBuffer, 0, numBytesRead);
1777 bufOldRevStream.close();
1780 randAccessContentFile.setLength(range.length);
1782 // Append data in request input stream to contentFile
1783 randAccessContentFile.seek(range.start);
1785 byte[] transferBuffer = new byte[BUFFER_SIZE];
1786 BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE);
1787 while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1)
1788 randAccessContentFile.write(transferBuffer, 0, numBytesRead);
1789 randAccessContentFile.close();
1790 requestBufInStream.close();
1797 * Serve the specified resource, optionally including the data content.
1799 * @param req The servlet request we are processing
1800 * @param resp The servlet response we are creating
1801 * @param content Should the content be included?
1802 * @exception IOException if an input/output error occurs
1803 * @exception ServletException if a servlet-specified error occurs
1804 * @throws RpcException
1805 * @throws InsufficientPermissionsException
1806 * @throws ObjectNotFoundException
1808 protected void serveResource(HttpServletRequest req, HttpServletResponse resp, boolean content) throws IOException, ServletException, ObjectNotFoundException, InsufficientPermissionsException, RpcException {
1810 // Identify the requested resource path
1811 String path = getRelativePath(req);
1812 if (logger.isDebugEnabled())
1814 logger.debug("Serving resource '" + path + "' headers and data");
1816 logger.debug("Serving resource '" + path + "' headers only");
1818 User user = getUser(req);
1819 boolean exists = true;
1820 Object resource = null;
1821 FileHeaderDTO file = null;
1822 FolderDTO folder = null;
1824 resource = getService().getResourceAtPath(user.getId(), path, true);
1825 } catch (ObjectNotFoundException e) {
1827 } catch (RpcException e) {
1828 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1833 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
1837 if (resource instanceof FolderDTO)
1838 folder = (FolderDTO) resource;
1840 file = (FileHeaderDTO) resource;
1842 // If the resource is not a collection, and the resource path
1843 // ends with "/" or "\", return NOT FOUND
1845 if (path.endsWith("/") || path.endsWith("\\")) {
1846 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
1850 // Check if the conditions specified in the optional If headers are
1853 // Checking If headers
1854 if (!checkIfHeaders(req, resp, file, null))
1857 // Find content type.
1858 String contentType = null;
1860 contentType = file.getMimeType();
1861 if (contentType == null) {
1862 contentType = getServletContext().getMimeType(file.getName());
1863 file.setMimeType(contentType);
1866 contentType = "text/html;charset=UTF-8";
1868 ArrayList ranges = null;
1869 long contentLength = -1L;
1872 // Accept ranges header
1873 resp.setHeader("Accept-Ranges", "bytes");
1874 // Parse range specifier
1875 ranges = parseRange(req, resp, file, null);
1877 resp.setHeader("ETag", getETag(file, null));
1878 // Last-Modified header
1879 resp.setHeader("Last-Modified", getLastModifiedHttp(file.getAuditInfo()));
1880 // Get content length
1881 contentLength = file.getFileSize();
1882 // Special case for zero length files, which would cause a
1883 // (silent) ISE when setting the output buffer size
1884 if (contentLength == 0L)
1888 ServletOutputStream ostream = null;
1889 PrintWriter writer = null;
1893 ostream = resp.getOutputStream();
1894 } catch (IllegalStateException e) {
1895 // If it fails, we try to get a Writer instead if we're
1896 // trying to serve a text file
1897 if (contentType == null || contentType.startsWith("text") || contentType.endsWith("xml"))
1898 writer = resp.getWriter();
1903 if (folder != null || (ranges == null || ranges.isEmpty()) && req.getHeader("Range") == null || ranges == FULL) {
1904 // Set the appropriate output headers
1905 if (contentType != null) {
1906 if (logger.isDebugEnabled())
1907 logger.debug("DefaultServlet.serveFile: contentType='" + contentType + "'");
1908 resp.setContentType(contentType);
1910 if (file != null && contentLength >= 0) {
1911 if (logger.isDebugEnabled())
1912 logger.debug("DefaultServlet.serveFile: contentLength=" + contentLength);
1913 if (contentLength < Integer.MAX_VALUE)
1914 resp.setContentLength((int) contentLength);
1916 // Set the content-length as String to be able to use a long
1917 resp.setHeader("content-length", "" + contentLength);
1920 InputStream renderResult = null;
1923 // Serve the directory browser
1924 renderResult = renderHtml(req.getContextPath(), path, folder, req);
1926 // Copy the input stream to our output stream (if requested)
1929 resp.setBufferSize(output);
1930 } catch (IllegalStateException e) {
1933 if (ostream != null)
1934 copy(file, renderResult, ostream, req, null);
1936 copy(file, renderResult, writer, req, null);
1937 updateAccounting(user, new Date(), contentLength);
1940 if (ranges == null || ranges.isEmpty())
1942 // Partial content response.
1943 resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
1945 if (ranges.size() == 1) {
1946 Range range = (Range) ranges.get(0);
1947 resp.addHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);
1948 long length = range.end - range.start + 1;
1949 if (length < Integer.MAX_VALUE)
1950 resp.setContentLength((int) length);
1952 // Set the content-length as String to be able to use a long
1953 resp.setHeader("content-length", "" + length);
1955 if (contentType != null) {
1956 if (logger.isDebugEnabled())
1957 logger.debug("DefaultServlet.serveFile: contentType='" + contentType + "'");
1958 resp.setContentType(contentType);
1963 resp.setBufferSize(output);
1964 } catch (IllegalStateException e) {
1967 if (ostream != null)
1968 copy(file, ostream, range, req, null);
1970 copy(file, writer, range, req, null);
1971 updateAccounting(user, new Date(), contentLength);
1976 resp.setContentType("multipart/byteranges; boundary=" + mimeSeparation);
1980 resp.setBufferSize(output);
1981 } catch (IllegalStateException e) {
1984 if (ostream != null)
1985 copy(file, ostream, ranges.iterator(), contentType, req, null);
1987 copy(file, writer, ranges.iterator(), contentType, req, null);
1997 * Retrieve the last modified date of a resource in HTTP format.
1999 * @param auditInfo the audit info for the specified resource
2000 * @return the last modified date in HTTP format
2002 protected String getLastModifiedHttp(AuditInfoDTO auditInfo) {
2003 Date modifiedDate = auditInfo.getModificationDate();
2004 if (modifiedDate == null)
2005 modifiedDate = auditInfo.getCreationDate();
2006 if (modifiedDate == null)
2007 modifiedDate = new Date();
2008 String lastModifiedHttp = null;
2009 synchronized (format) {
2010 lastModifiedHttp = format.format(modifiedDate);
2012 return lastModifiedHttp;
2016 * Parse the range header.
2018 * @param request The servlet request we are processing
2019 * @param response The servlet response we are creating
2021 * @param oldBody the old version of the file, if requested
2022 * @return Vector of ranges
2023 * @throws IOException
2025 protected ArrayList parseRange(HttpServletRequest request, HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
2026 // Checking If-Range
2027 String headerValue = request.getHeader("If-Range");
2028 if (headerValue != null) {
2029 long headerValueTime = -1L;
2031 headerValueTime = request.getDateHeader("If-Range");
2032 } catch (IllegalArgumentException e) {
2036 String eTag = getETag(file, oldBody);
2037 long lastModified = oldBody == null ?
2038 file.getAuditInfo().getModificationDate().getTime() :
2039 oldBody.getAuditInfo().getModificationDate().getTime();
2041 if (headerValueTime == -1L) {
2042 // If the ETag the client gave does not match the entity
2043 // etag, then the entire entity is returned.
2044 if (!eTag.equals(headerValue.trim()))
2047 // If the timestamp of the entity the client got is older than
2048 // the last modification date of the entity, the entire entity
2050 if (lastModified > headerValueTime + 1000)
2054 long fileLength = oldBody == null ? file.getFileSize() : oldBody.getFileSize();
2055 if (fileLength == 0)
2058 // Retrieving the range header (if any is specified).
2059 String rangeHeader = request.getHeader("Range");
2061 if (rangeHeader == null)
2063 // bytes is the only range unit supported (and I don't see the point
2064 // of adding new ones).
2065 if (!rangeHeader.startsWith("bytes")) {
2066 response.addHeader("Content-Range", "bytes */" + fileLength);
2067 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2071 rangeHeader = rangeHeader.substring(6);
2073 // Vector that will contain all the ranges which are successfully
2075 ArrayList<Range> result = new ArrayList<Range>();
2076 StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
2077 // Parsing the range list
2078 while (commaTokenizer.hasMoreTokens()) {
2079 String rangeDefinition = commaTokenizer.nextToken().trim();
2081 Range currentRange = new Range();
2082 currentRange.length = fileLength;
2084 int dashPos = rangeDefinition.indexOf('-');
2086 if (dashPos == -1) {
2087 response.addHeader("Content-Range", "bytes */" + fileLength);
2088 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2094 long offset = Long.parseLong(rangeDefinition);
2095 currentRange.start = fileLength + offset;
2096 currentRange.end = fileLength - 1;
2097 } catch (NumberFormatException e) {
2098 response.addHeader("Content-Range", "bytes */" + fileLength);
2099 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2104 currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
2105 if (dashPos < rangeDefinition.length() - 1)
2106 currentRange.end = Long.parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length()));
2108 currentRange.end = fileLength - 1;
2109 } catch (NumberFormatException e) {
2110 response.addHeader("Content-Range", "bytes */" + fileLength);
2111 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2115 if (!currentRange.validate()) {
2116 response.addHeader("Content-Range", "bytes */" + fileLength);
2117 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2120 result.add(currentRange);
2126 * Check if the conditions specified in the optional If headers are
2129 * @param request The servlet request we are processing
2130 * @param response The servlet response we are creating
2131 * @param file the file resource against which the checks will be made
2132 * @param oldBody the old version of the file, if requested
2133 * @return boolean true if the resource meets all the specified conditions,
2134 * and false if any of the conditions is not satisfied, in which
2135 * case request processing is stopped
2136 * @throws IOException
2138 protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response,
2139 FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
2140 // TODO : Checking the WebDAV If header
2141 return checkIfMatch(request, response, file, oldBody) &&
2142 checkIfModifiedSince(request, response, file, oldBody) &&
2143 checkIfNoneMatch(request, response, file, oldBody) &&
2144 checkIfUnmodifiedSince(request, response, file, oldBody);
2148 * Check if the if-match condition is satisfied.
2150 * @param request The servlet request we are processing
2151 * @param response The servlet response we are creating
2152 * @param file the file object
2153 * @param oldBody the old version of the file, if requested
2154 * @return boolean true if the resource meets the specified condition, and
2155 * false if the condition is not satisfied, in which case request
2156 * processing is stopped
2157 * @throws IOException
2159 private boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response,
2160 FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
2161 String eTag = getETag(file, oldBody);
2162 String headerValue = request.getHeader("If-Match");
2163 if (headerValue != null)
2164 if (headerValue.indexOf('*') == -1) {
2165 StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
2166 boolean conditionSatisfied = false;
2167 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
2168 String currentToken = commaTokenizer.nextToken();
2169 if (currentToken.trim().equals(eTag))
2170 conditionSatisfied = true;
2172 // If none of the given ETags match, 412 Precodition failed is
2174 if (!conditionSatisfied) {
2175 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2183 * Check if the if-modified-since condition is satisfied.
2185 * @param request The servlet request we are processing
2186 * @param response The servlet response we are creating
2187 * @param file the file object
2188 * @param oldBody the old version of the file, if requested
2189 * @return boolean true if the resource meets the specified condition, and
2190 * false if the condition is not satisfied, in which case request
2191 * processing is stopped
2193 private boolean checkIfModifiedSince(HttpServletRequest request,
2194 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody) {
2196 long headerValue = request.getDateHeader("If-Modified-Since");
2197 long lastModified = oldBody == null ?
2198 file.getAuditInfo().getModificationDate().getTime() :
2199 oldBody.getAuditInfo().getModificationDate().getTime();
2200 if (headerValue != -1)
2201 // If an If-None-Match header has been specified, if modified
2202 // since is ignored.
2203 if (request.getHeader("If-None-Match") == null && lastModified < headerValue + 1000) {
2204 // The entity has not been modified since the date
2205 // specified by the client. This is not an error case.
2206 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
2207 response.setHeader("ETag", getETag(file, oldBody));
2210 } catch (IllegalArgumentException illegalArgument) {
2217 * Check if the if-none-match condition is satisfied.
2219 * @param request The servlet request we are processing
2220 * @param response The servlet response we are creating
2221 * @param file the file object
2222 * @param oldBody the old version of the file, if requested
2223 * @return boolean true if the resource meets the specified condition, and
2224 * false if the condition is not satisfied, in which case request
2225 * processing is stopped
2226 * @throws IOException
2228 private boolean checkIfNoneMatch(HttpServletRequest request,
2229 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody)
2230 throws IOException {
2231 String eTag = getETag(file, oldBody);
2232 String headerValue = request.getHeader("If-None-Match");
2233 if (headerValue != null) {
2234 boolean conditionSatisfied = false;
2235 if (!headerValue.equals("*")) {
2236 StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
2237 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
2238 String currentToken = commaTokenizer.nextToken();
2239 if (currentToken.trim().equals(eTag))
2240 conditionSatisfied = true;
2243 conditionSatisfied = true;
2244 if (conditionSatisfied) {
2245 // For GET and HEAD, we should respond with 304 Not Modified.
2246 // For every other method, 412 Precondition Failed is sent
2248 if ("GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod())) {
2249 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
2250 response.setHeader("ETag", getETag(file, oldBody));
2253 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2261 * Check if the if-unmodified-since condition is satisfied.
2263 * @param request The servlet request we are processing
2264 * @param response The servlet response we are creating
2265 * @param file the file object
2266 * @param oldBody the old version of the file, if requested
2267 * @return boolean true if the resource meets the specified condition, and
2268 * false if the condition is not satisfied, in which case request
2269 * processing is stopped
2270 * @throws IOException
2272 private boolean checkIfUnmodifiedSince(HttpServletRequest request,
2273 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody)
2274 throws IOException {
2276 long lastModified = oldBody == null ?
2277 file.getAuditInfo().getModificationDate().getTime() :
2278 oldBody.getAuditInfo().getModificationDate().getTime();
2279 long headerValue = request.getDateHeader("If-Unmodified-Since");
2280 if (headerValue != -1)
2281 if (lastModified >= headerValue + 1000) {
2282 // The entity has not been modified since the date
2283 // specified by the client. This is not an error case.
2284 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2287 } catch (IllegalArgumentException illegalArgument) {
2294 * Copy the contents of the specified input stream to the specified output
2295 * stream, and ensure that both streams are closed before returning (even in
2296 * the face of an exception).
2298 * @param file the file resource
2300 * @param ostream The output stream to write to
2301 * @param req the HTTP request
2302 * @param oldBody the old version of the file, if requested
2303 * @exception IOException if an input/output error occurs
2304 * @throws RpcException
2305 * @throws InsufficientPermissionsException
2306 * @throws ObjectNotFoundException
2308 protected void copy(FileHeaderDTO file, InputStream is, ServletOutputStream ostream,
2309 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2310 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2311 IOException exception = null;
2312 InputStream resourceInputStream = null;
2313 User user = getUser(req);
2314 // Files open for all will not have specified a calling user in the request.
2316 user = getOwner(req);
2318 throw new ObjectNotFoundException("No user or owner specified");
2320 resourceInputStream = oldBody == null ?
2321 getService().getFileContents(user.getId(), file.getId()) :
2322 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2324 resourceInputStream = is;
2326 InputStream istream = new BufferedInputStream(resourceInputStream, input);
2327 // Copy the input stream to the output stream
2328 exception = copyRange(istream, ostream);
2329 // Clean up the input stream
2331 // Rethrow any exception that has occurred
2332 if (exception != null)
2337 * Copy the contents of the specified input stream to the specified output
2338 * stream, and ensure that both streams are closed before returning (even in
2339 * the face of an exception).
2341 * @param istream The input stream to read from
2342 * @param ostream The output stream to write to
2343 * @return Exception which occurred during processing
2345 private IOException copyRange(InputStream istream, ServletOutputStream ostream) {
2346 // Copy the input stream to the output stream
2347 IOException exception = null;
2348 byte buffer[] = new byte[input];
2349 int len = buffer.length;
2352 len = istream.read(buffer);
2355 ostream.write(buffer, 0, len);
2356 } catch (IOException e) {
2365 * Copy the contents of the specified input stream to the specified output
2366 * stream, and ensure that both streams are closed before returning (even in
2367 * the face of an exception).
2371 * @param resourceInfo The resource info
2372 * @param writer The writer to write to
2373 * @param req the HTTP request
2374 * @param oldBody the old version of the file, if requested
2375 * @exception IOException if an input/output error occurs
2376 * @throws RpcException
2377 * @throws InsufficientPermissionsException
2378 * @throws ObjectNotFoundException
2380 protected void copy(FileHeaderDTO file, InputStream is, PrintWriter writer,
2381 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2382 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2383 IOException exception = null;
2385 User user = getUser(req);
2386 InputStream resourceInputStream = null;
2388 resourceInputStream = oldBody == null ?
2389 getService().getFileContents(user.getId(), file.getId()) :
2390 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2392 resourceInputStream = is;
2395 if (fileEncoding == null)
2396 reader = new InputStreamReader(resourceInputStream);
2398 reader = new InputStreamReader(resourceInputStream, fileEncoding);
2400 // Copy the input stream to the output stream
2401 exception = copyRange(reader, writer);
2402 // Clean up the reader
2404 // Rethrow any exception that has occurred
2405 if (exception != null)
2410 * Copy the contents of the specified input stream to the specified output
2411 * stream, and ensure that both streams are closed before returning (even in
2412 * the face of an exception).
2414 * @param reader The reader to read from
2415 * @param writer The writer to write to
2416 * @return Exception which occurred during processing
2418 private IOException copyRange(Reader reader, PrintWriter writer) {
2419 // Copy the input stream to the output stream
2420 IOException exception = null;
2421 char buffer[] = new char[input];
2422 int len = buffer.length;
2425 len = reader.read(buffer);
2428 writer.write(buffer, 0, len);
2429 } catch (IOException e) {
2438 * Copy the contents of the specified input stream to the specified output
2439 * stream, and ensure that both streams are closed before returning (even in
2440 * the face of an exception).
2443 * @param writer The writer to write to
2444 * @param ranges Enumeration of the ranges the client wanted to retrieve
2445 * @param contentType Content type of the resource
2446 * @param req the HTTP request
2447 * @param oldBody the old version of the file, if requested
2448 * @exception IOException if an input/output error occurs
2449 * @throws RpcException
2450 * @throws InsufficientPermissionsException
2451 * @throws ObjectNotFoundException
2453 protected void copy(FileHeaderDTO file, PrintWriter writer, Iterator ranges,
2454 String contentType, HttpServletRequest req, FileBodyDTO oldBody)
2455 throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2456 User user = getUser(req);
2457 IOException exception = null;
2458 while (exception == null && ranges.hasNext()) {
2459 InputStream resourceInputStream = oldBody == null ?
2460 getService().getFileContents(user.getId(), file.getId()) :
2461 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2463 if (fileEncoding == null)
2464 reader = new InputStreamReader(resourceInputStream);
2466 reader = new InputStreamReader(resourceInputStream, fileEncoding);
2467 Range currentRange = (Range) ranges.next();
2468 // Writing MIME header.
2470 writer.println("--" + mimeSeparation);
2471 if (contentType != null)
2472 writer.println("Content-Type: " + contentType);
2473 writer.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" + currentRange.length);
2476 exception = copyRange(reader, writer, currentRange.start, currentRange.end);
2480 writer.print("--" + mimeSeparation + "--");
2481 // Rethrow any exception that has occurred
2482 if (exception != null)
2487 * Copy the contents of the specified input stream to the specified output
2488 * stream, and ensure that both streams are closed before returning (even in
2489 * the face of an exception).
2491 * @param istream The input stream to read from
2492 * @param ostream The output stream to write to
2493 * @param start Start of the range which will be copied
2494 * @param end End of the range which will be copied
2495 * @return Exception which occurred during processing
2497 private IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) {
2498 if (logger.isDebugEnabled())
2499 logger.debug("Serving bytes:" + start + "-" + end);
2501 istream.skip(start);
2502 } catch (IOException e) {
2505 IOException exception = null;
2506 long bytesToRead = end - start + 1;
2507 byte buffer[] = new byte[input];
2508 int len = buffer.length;
2509 while (bytesToRead > 0 && len >= buffer.length) {
2511 len = istream.read(buffer);
2512 if (bytesToRead >= len) {
2513 ostream.write(buffer, 0, len);
2516 ostream.write(buffer, 0, (int) bytesToRead);
2519 } catch (IOException e) {
2523 if (len < buffer.length)
2530 * Copy the contents of the specified input stream to the specified output
2531 * stream, and ensure that both streams are closed before returning (even in
2532 * the face of an exception).
2534 * @param reader The reader to read from
2535 * @param writer The writer to write to
2536 * @param start Start of the range which will be copied
2537 * @param end End of the range which will be copied
2538 * @return Exception which occurred during processing
2540 private IOException copyRange(Reader reader, PrintWriter writer, long start, long end) {
2543 } catch (IOException e) {
2546 IOException exception = null;
2547 long bytesToRead = end - start + 1;
2548 char buffer[] = new char[input];
2549 int len = buffer.length;
2550 while (bytesToRead > 0 && len >= buffer.length) {
2552 len = reader.read(buffer);
2553 if (bytesToRead >= len) {
2554 writer.write(buffer, 0, len);
2557 writer.write(buffer, 0, (int) bytesToRead);
2560 } catch (IOException e) {
2564 if (len < buffer.length)
2571 * Copy the contents of the specified input stream to the specified output
2572 * stream, and ensure that both streams are closed before returning (even in
2573 * the face of an exception).
2576 * @param ostream The output stream to write to
2577 * @param range Range the client wanted to retrieve
2578 * @param req the HTTP request
2579 * @param oldBody the old version of the file, if requested
2580 * @exception IOException if an input/output error occurs
2581 * @throws RpcException
2582 * @throws InsufficientPermissionsException
2583 * @throws ObjectNotFoundException
2585 protected void copy(FileHeaderDTO file, ServletOutputStream ostream, Range range,
2586 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2587 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2588 IOException exception = null;
2589 User user = getUser(req);
2590 InputStream resourceInputStream = oldBody == null ?
2591 getService().getFileContents(user.getId(), file.getId()) :
2592 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2593 InputStream istream = new BufferedInputStream(resourceInputStream, input);
2594 exception = copyRange(istream, ostream, range.start, range.end);
2595 // Clean up the input stream
2597 // Rethrow any exception that has occurred
2598 if (exception != null)
2603 * Copy the contents of the specified input stream to the specified output
2604 * stream, and ensure that both streams are closed before returning (even in
2605 * the face of an exception).
2608 * @param writer The writer to write to
2609 * @param range Range the client wanted to retrieve
2610 * @param req the HTTP request
2611 * @param oldBody the old version of the file, if requested
2612 * @exception IOException if an input/output error occurs
2613 * @throws RpcException
2614 * @throws InsufficientPermissionsException
2615 * @throws ObjectNotFoundException
2617 protected void copy(FileHeaderDTO file, PrintWriter writer, Range range,
2618 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2619 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2620 IOException exception = null;
2621 User user = getUser(req);
2622 InputStream resourceInputStream = oldBody == null ?
2623 getService().getFileContents(user.getId(), file.getId()) :
2624 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2626 if (fileEncoding == null)
2627 reader = new InputStreamReader(resourceInputStream);
2629 reader = new InputStreamReader(resourceInputStream, fileEncoding);
2631 exception = copyRange(reader, writer, range.start, range.end);
2632 // Clean up the input stream
2634 // Rethrow any exception that has occurred
2635 if (exception != null)
2640 * Copy the contents of the specified input stream to the specified output
2641 * stream, and ensure that both streams are closed before returning (even in
2642 * the face of an exception).
2645 * @param ostream The output stream to write to
2646 * @param ranges Enumeration of the ranges the client wanted to retrieve
2647 * @param contentType Content type of the resource
2648 * @param req the HTTP request
2649 * @param oldBody the old version of the file, if requested
2650 * @exception IOException if an input/output error occurs
2651 * @throws RpcException
2652 * @throws InsufficientPermissionsException
2653 * @throws ObjectNotFoundException
2655 protected void copy(FileHeaderDTO file, ServletOutputStream ostream,
2656 Iterator ranges, String contentType, HttpServletRequest req,
2657 FileBodyDTO oldBody) throws IOException, ObjectNotFoundException,
2658 InsufficientPermissionsException, RpcException {
2659 IOException exception = null;
2660 User user = getUser(req);
2661 while (exception == null && ranges.hasNext()) {
2662 InputStream resourceInputStream = oldBody == null ?
2663 getService().getFileContents(user.getId(), file.getId()) :
2664 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2665 InputStream istream = new BufferedInputStream(resourceInputStream, input);
2666 Range currentRange = (Range) ranges.next();
2667 // Writing MIME header.
2669 ostream.println("--" + mimeSeparation);
2670 if (contentType != null)
2671 ostream.println("Content-Type: " + contentType);
2672 ostream.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" + currentRange.length);
2676 exception = copyRange(istream, ostream, currentRange.start, currentRange.end);
2681 ostream.print("--" + mimeSeparation + "--");
2682 // Rethrow any exception that has occurred
2683 if (exception != null)
2688 * Return an InputStream to an HTML representation of the contents of this
2691 * @param contextPath Context path to which our internal paths are relative
2692 * @param path the requested path to the resource
2693 * @param folder the specified directory
2694 * @param req the HTTP request
2695 * @return an input stream with the rendered contents
2696 * @throws IOException
2697 * @throws ServletException
2699 private InputStream renderHtml(String contextPath, String path, FolderDTO folder, HttpServletRequest req) throws IOException, ServletException {
2700 String name = folder.getName();
2701 // Prepare a writer to a buffered area
2702 ByteArrayOutputStream stream = new ByteArrayOutputStream();
2703 OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
2704 PrintWriter writer = new PrintWriter(osWriter);
2705 StringBuffer sb = new StringBuffer();
2706 // rewriteUrl(contextPath) is expensive. cache result for later reuse
2707 String rewrittenContextPath = rewriteUrl(contextPath);
2708 // Render the page header
2709 sb.append("<html>\r\n");
2710 sb.append("<head>\r\n");
2711 sb.append("<title>");
2712 sb.append("Index of " + name);
2713 sb.append("</title>\r\n");
2714 sb.append("<STYLE><!--");
2716 sb.append("--></STYLE> ");
2717 sb.append("</head>\r\n");
2718 sb.append("<body>");
2720 sb.append("Index of " + name);
2722 // Render the link to our parent (if required)
2723 String parentDirectory = path;
2724 if (parentDirectory.endsWith("/"))
2725 parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1);
2726 int slash = parentDirectory.lastIndexOf('/');
2728 String parent = path.substring(0, slash);
2729 sb.append(" - <a href=\"");
2730 sb.append(rewrittenContextPath);
2731 if (parent.equals(""))
2733 sb.append(rewriteUrl(parent));
2734 if (!parent.endsWith("/"))
2738 sb.append("Up To " + parent);
2744 sb.append("<HR size=\"1\" noshade=\"noshade\">");
2746 sb.append("<table width=\"100%\" cellspacing=\"0\"" + " cellpadding=\"5\" align=\"center\">\r\n");
2748 // Render the column headings
2749 sb.append("<tr>\r\n");
2750 sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
2752 sb.append("</strong></font></td>\r\n");
2753 sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
2755 sb.append("</strong></font></td>\r\n");
2756 sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
2757 sb.append("Last modified");
2758 sb.append("</strong></font></td>\r\n");
2760 // Render the directory entries within this directory
2761 boolean shade = false;
2762 Iterator iter = folder.getSubfolders().iterator();
2763 while (iter.hasNext()) {
2764 FolderDTO subf = (FolderDTO) iter.next();
2765 String resourceName = subf.getName();
2766 if (resourceName.equalsIgnoreCase("WEB-INF") || resourceName.equalsIgnoreCase("META-INF"))
2771 sb.append(" bgcolor=\"#eeeeee\"");
2775 sb.append("<td align=\"left\"> \r\n");
2776 sb.append("<a href=\"");
2777 sb.append(rewrittenContextPath);
2778 sb.append(rewriteUrl(path + resourceName));
2780 sb.append("\"><tt>");
2781 sb.append(RequestUtil.filter(resourceName));
2783 sb.append("</tt></a></td>\r\n");
2785 sb.append("<td align=\"right\"><tt>");
2786 sb.append(" ");
2787 sb.append("</tt></td>\r\n");
2789 sb.append("<td align=\"right\"><tt>");
2790 sb.append(getLastModifiedHttp(folder.getAuditInfo()));
2791 sb.append("</tt></td>\r\n");
2793 sb.append("</tr>\r\n");
2795 List<FileHeaderDTO> files;
2797 User user = getUser(req);
2798 files = getService().getFiles(user.getId(), folder.getId(), true);
2799 } catch (ObjectNotFoundException e) {
2800 throw new ServletException(e.getMessage());
2801 } catch (InsufficientPermissionsException e) {
2802 throw new ServletException(e.getMessage());
2803 } catch (RpcException e) {
2804 throw new ServletException(e.getMessage());
2806 for (FileHeaderDTO file : files) {
2807 String resourceName = file.getName();
2808 if (resourceName.equalsIgnoreCase("WEB-INF") || resourceName.equalsIgnoreCase("META-INF"))
2813 sb.append(" bgcolor=\"#eeeeee\"");
2817 sb.append("<td align=\"left\"> \r\n");
2818 sb.append("<a href=\"");
2819 sb.append(rewrittenContextPath);
2820 sb.append(rewriteUrl(path + resourceName));
2821 sb.append("\"><tt>");
2822 sb.append(RequestUtil.filter(resourceName));
2823 sb.append("</tt></a></td>\r\n");
2825 sb.append("<td align=\"right\"><tt>");
2826 sb.append(renderSize(file.getFileSize()));
2827 sb.append("</tt></td>\r\n");
2829 sb.append("<td align=\"right\"><tt>");
2830 sb.append(getLastModifiedHttp(file.getAuditInfo()));
2831 sb.append("</tt></td>\r\n");
2833 sb.append("</tr>\r\n");
2836 // Render the page footer
2837 sb.append("</table>\r\n");
2839 sb.append("<HR size=\"1\" noshade=\"noshade\">");
2841 sb.append("<h3>").append(getServletContext().getServerInfo()).append("</h3>");
2842 sb.append("</body>\r\n");
2843 sb.append("</html>\r\n");
2845 // Return an input stream to the underlying bytes
2846 writer.write(sb.toString());
2848 return new ByteArrayInputStream(stream.toByteArray());
2853 * Render the specified file size (in bytes).
2855 * @param size File size (in bytes)
2856 * @return the size as a string
2858 private String renderSize(long size) {
2859 long leftSide = size / 1024;
2860 long rightSide = size % 1024 / 103; // Makes 1 digit
2861 if (leftSide == 0 && rightSide == 0 && size > 0)
2863 return "" + leftSide + "." + rightSide + " kb";
2869 * @param req Servlet request
2870 * @param resp Servlet response
2871 * @return boolean true if the copy is successful
2872 * @throws IOException
2874 private boolean copyResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
2875 // Parsing destination header
2876 String destinationPath = req.getHeader("Destination");
2877 if (destinationPath == null) {
2878 resp.sendError(WebdavStatus.SC_BAD_REQUEST);
2882 // Remove url encoding from destination
2883 destinationPath = RequestUtil.URLDecode(destinationPath, "UTF8");
2885 int protocolIndex = destinationPath.indexOf("://");
2886 if (protocolIndex >= 0) {
2887 // if the Destination URL contains the protocol, we can safely
2888 // trim everything upto the first "/" character after "://"
2889 int firstSeparator = destinationPath.indexOf("/", protocolIndex + 4);
2890 if (firstSeparator < 0)
2891 destinationPath = "/";
2893 destinationPath = destinationPath.substring(firstSeparator);
2895 String hostName = req.getServerName();
2896 if (hostName != null && destinationPath.startsWith(hostName))
2897 destinationPath = destinationPath.substring(hostName.length());
2899 int portIndex = destinationPath.indexOf(":");
2901 destinationPath = destinationPath.substring(portIndex);
2903 if (destinationPath.startsWith(":")) {
2904 int firstSeparator = destinationPath.indexOf("/");
2905 if (firstSeparator < 0)
2906 destinationPath = "/";
2908 destinationPath = destinationPath.substring(firstSeparator);
2912 // Normalize destination path (remove '.' and '..')
2913 destinationPath = RequestUtil.normalize(destinationPath);
2915 String contextPath = req.getContextPath();
2916 if (contextPath != null && destinationPath.startsWith(contextPath))
2917 destinationPath = destinationPath.substring(contextPath.length());
2919 String pathInfo = req.getPathInfo();
2920 if (pathInfo != null) {
2921 String servletPath = req.getServletPath();
2922 if (servletPath != null && destinationPath.startsWith(servletPath))
2923 destinationPath = destinationPath.substring(servletPath.length());
2926 if (logger.isDebugEnabled())
2927 logger.debug("Dest path :" + destinationPath);
2929 if (destinationPath.toUpperCase().startsWith("/WEB-INF") || destinationPath.toUpperCase().startsWith("/META-INF")) {
2930 resp.sendError(WebdavStatus.SC_FORBIDDEN);
2934 String path = getRelativePath(req);
2936 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
2937 resp.sendError(WebdavStatus.SC_FORBIDDEN);
2941 if (destinationPath.equals(path)) {
2942 resp.sendError(WebdavStatus.SC_FORBIDDEN);
2946 // Parsing overwrite header
2947 boolean overwrite = true;
2948 String overwriteHeader = req.getHeader("Overwrite");
2950 if (overwriteHeader != null)
2951 if (overwriteHeader.equalsIgnoreCase("T"))
2956 User user = getUser(req);
2957 // Overwriting the destination
2958 boolean exists = true;
2960 getService().getResourceAtPath(user.getId(), destinationPath, true);
2961 } catch (ObjectNotFoundException e) {
2963 } catch (RpcException e) {
2964 resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
2969 // Delete destination resource, if it exists
2971 if (!deleteResource(destinationPath, req, resp, true))
2974 resp.setStatus(WebdavStatus.SC_CREATED);
2975 } else // If the destination exists, then it's a conflict
2977 resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);
2980 resp.setStatus(WebdavStatus.SC_CREATED);
2982 // Copying source to destination.
2983 Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();
2986 result = copyResource(errorList, path, destinationPath, req);
2987 } catch (RpcException e) {
2988 resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
2991 if (!result || !errorList.isEmpty()) {
2992 sendReport(req, resp, errorList);
2999 * Copy a collection.
3001 * @param errorList Hashtable containing the list of errors which occurred
3002 * during the copy operation
3003 * @param source Path of the resource to be copied
3004 * @param theDest Destination path
3005 * @param req the HTTP request
3006 * @return boolean true if the copy is successful
3007 * @throws RpcException
3009 private boolean copyResource(Hashtable<String, Integer> errorList, String source, String theDest, HttpServletRequest req) throws RpcException {
3011 String dest = theDest;
3012 // Fix the destination path when copying collections.
3013 if (source.endsWith("/") && !dest.endsWith("/"))
3016 if (logger.isDebugEnabled())
3017 logger.debug("Copy: " + source + " To: " + dest);
3019 final User user = getUser(req);
3020 Object object = null;
3022 object = getService().getResourceAtPath(user.getId(), source, true);
3023 } catch (ObjectNotFoundException e) {
3026 if (object instanceof FolderDTO) {
3027 final FolderDTO folder = (FolderDTO) object;
3029 final String des = dest;
3030 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3032 public Void call() throws Exception {
3033 getService().copyFolder(user.getId(), folder.getId(), des);
3037 } catch (ObjectNotFoundException e) {
3038 errorList.put(dest, new Integer(WebdavStatus.SC_CONFLICT));
3040 } catch (DuplicateNameException e) {
3041 errorList.put(dest, new Integer(WebdavStatus.SC_CONFLICT));
3043 } catch (InsufficientPermissionsException e) {
3044 errorList.put(dest, new Integer(WebdavStatus.SC_FORBIDDEN));
3046 } catch (Exception e) {
3047 errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3052 String newSource = source;
3053 if (!source.endsWith("/"))
3055 String newDest = dest;
3056 if (!dest.endsWith("/"))
3058 // Recursively copy the subfolders.
3059 Iterator iter = folder.getSubfolders().iterator();
3060 while (iter.hasNext()) {
3061 FolderDTO subf = (FolderDTO) iter.next();
3062 String resourceName = subf.getName();
3063 copyResource(errorList, newSource + resourceName, newDest + resourceName, req);
3065 // Recursively copy the files.
3066 List<FileHeaderDTO> files;
3067 files = getService().getFiles(user.getId(), folder.getId(), true);
3068 for (FileHeaderDTO file : files) {
3069 String resourceName = file.getName();
3070 copyResource(errorList, newSource + resourceName, newDest + resourceName, req);
3072 } catch (RpcException e) {
3073 errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3075 } catch (ObjectNotFoundException e) {
3076 errorList.put(source, new Integer(WebdavStatus.SC_NOT_FOUND));
3078 } catch (InsufficientPermissionsException e) {
3079 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3083 } else if (object instanceof FileHeaderDTO) {
3084 final FileHeaderDTO file = (FileHeaderDTO) object;
3086 final String des = dest;
3087 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3089 public Void call() throws Exception {
3090 getService().copyFile(user.getId(), file.getId(), des);
3094 } catch (ObjectNotFoundException e) {
3095 errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3097 } catch (DuplicateNameException e) {
3098 errorList.put(source, new Integer(WebdavStatus.SC_CONFLICT));
3100 } catch (InsufficientPermissionsException e) {
3101 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3103 } catch (QuotaExceededException e) {
3104 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3106 } catch (GSSIOException e) {
3107 errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3109 } catch (Exception e) {
3110 errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3114 errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3121 * Delete a resource.
3123 * @param req Servlet request
3124 * @param resp Servlet response
3125 * @return boolean true if the deletion is successful
3126 * @throws IOException
3128 private boolean deleteResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
3129 String path = getRelativePath(req);
3130 return deleteResource(path, req, resp, true);
3134 * Delete a resource.
3136 * @param path Path of the resource which is to be deleted
3137 * @param req Servlet request
3138 * @param resp Servlet response
3139 * @param setStatus Should the response status be set on successful
3141 * @return boolean true if the deletion is successful
3142 * @throws IOException
3144 private boolean deleteResource(String path, HttpServletRequest req, HttpServletResponse resp, boolean setStatus) throws IOException {
3145 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
3146 resp.sendError(WebdavStatus.SC_FORBIDDEN);
3149 String ifHeader = req.getHeader("If");
3150 if (ifHeader == null)
3153 String lockTokenHeader = req.getHeader("Lock-Token");
3154 if (lockTokenHeader == null)
3155 lockTokenHeader = "";
3157 if (isLocked(path, ifHeader + lockTokenHeader)) {
3158 resp.sendError(WebdavStatus.SC_LOCKED);
3162 final User user = getUser(req);
3163 boolean exists = true;
3164 Object object = null;
3166 object = getService().getResourceAtPath(user.getId(), path, true);
3167 } catch (ObjectNotFoundException e) {
3169 } catch (RpcException e) {
3170 resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
3175 resp.sendError(WebdavStatus.SC_NOT_FOUND);
3179 FolderDTO folder = null;
3180 FileHeaderDTO file = null;
3181 if (object instanceof FolderDTO)
3182 folder = (FolderDTO) object;
3184 file = (FileHeaderDTO) object;
3188 final FileHeaderDTO f = file;
3189 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3191 public Void call() throws Exception {
3192 getService().deleteFile(user.getId(), f.getId());
3196 } catch (InsufficientPermissionsException e) {
3197 resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
3199 } catch (ObjectNotFoundException e) {
3200 // Although we had already found the object, it was
3201 // probably deleted from another thread.
3202 resp.sendError(WebdavStatus.SC_NOT_FOUND);
3204 } catch (RpcException e) {
3205 resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
3207 } catch (Exception e) {
3208 resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
3211 else if (folder != null) {
3212 Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();
3213 deleteCollection(req, folder, path, errorList);
3215 final FolderDTO f = folder;
3216 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3218 public Void call() throws Exception {
3219 getService().deleteFolder(user.getId(), f.getId());
3223 } catch (InsufficientPermissionsException e) {
3224 errorList.put(path, new Integer(WebdavStatus.SC_METHOD_NOT_ALLOWED));
3225 } catch (ObjectNotFoundException e) {
3226 errorList.put(path, new Integer(WebdavStatus.SC_NOT_FOUND));
3227 } catch (RpcException e) {
3228 errorList.put(path, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3229 } catch (Exception e) {
3230 errorList.put(path, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3233 if (!errorList.isEmpty()) {
3234 sendReport(req, resp, errorList);
3239 resp.setStatus(WebdavStatus.SC_NO_CONTENT);
3244 * Deletes a collection.
3246 * @param req the HTTP request
3247 * @param folder the folder whose contents will be deleted
3248 * @param path Path to the collection to be deleted
3249 * @param errorList Contains the list of the errors which occurred
3251 private void deleteCollection(HttpServletRequest req, FolderDTO folder, String path, Hashtable<String, Integer> errorList) {
3253 if (logger.isDebugEnabled())
3254 logger.debug("Delete:" + path);
3256 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
3257 errorList.put(path, new Integer(WebdavStatus.SC_FORBIDDEN));
3261 String ifHeader = req.getHeader("If");
3262 if (ifHeader == null)
3265 String lockTokenHeader = req.getHeader("Lock-Token");
3266 if (lockTokenHeader == null)
3267 lockTokenHeader = "";
3269 Iterator iter = folder.getSubfolders().iterator();
3270 while (iter.hasNext()) {
3271 FolderDTO subf = (FolderDTO) iter.next();
3272 String childName = path;
3273 if (!childName.equals("/"))
3275 childName += subf.getName();
3277 if (isLocked(childName, ifHeader + lockTokenHeader))
3278 errorList.put(childName, new Integer(WebdavStatus.SC_LOCKED));
3281 final User user = getUser(req);
3282 Object object = getService().getResourceAtPath(user.getId(), childName, true);
3283 FolderDTO childFolder = null;
3284 FileHeaderDTO childFile = null;
3285 if (object instanceof FolderDTO)
3286 childFolder = (FolderDTO) object;
3288 childFile = (FileHeaderDTO) object;
3289 if (childFolder != null) {
3290 final FolderDTO cf = childFolder;
3291 deleteCollection(req, childFolder, childName, errorList);
3292 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3294 public Void call() throws Exception {
3295 getService().deleteFolder(user.getId(), cf.getId());
3299 } else if (childFile != null) {
3300 final FileHeaderDTO cf = childFile;
3301 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3303 public Void call() throws Exception {
3304 getService().deleteFile(user.getId(), cf.getId());
3309 } catch (ObjectNotFoundException e) {
3310 errorList.put(childName, new Integer(WebdavStatus.SC_NOT_FOUND));
3311 } catch (InsufficientPermissionsException e) {
3312 errorList.put(childName, new Integer(WebdavStatus.SC_FORBIDDEN));
3313 } catch (RpcException e) {
3314 errorList.put(childName, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3315 } catch (Exception e) {
3316 errorList.put(childName, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3322 * Send a multistatus element containing a complete error report to the
3325 * @param req Servlet request
3326 * @param resp Servlet response
3327 * @param errorList List of error to be displayed
3328 * @throws IOException
3330 private void sendReport(HttpServletRequest req, HttpServletResponse resp, Hashtable errorList) throws IOException {
3332 resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
3334 String absoluteUri = req.getRequestURI();
3335 String relativePath = getRelativePath(req);
3337 XMLWriter generatedXML = new XMLWriter();
3338 generatedXML.writeXMLHeader();
3340 generatedXML.writeElement(null, "multistatus" + generateNamespaceDeclarations(), XMLWriter.OPENING);
3342 Enumeration pathList = errorList.keys();
3343 while (pathList.hasMoreElements()) {
3345 String errorPath = (String) pathList.nextElement();
3346 int errorCode = ((Integer) errorList.get(errorPath)).intValue();
3348 generatedXML.writeElement(null, "response", XMLWriter.OPENING);
3350 generatedXML.writeElement(null, "href", XMLWriter.OPENING);
3351 String toAppend = errorPath.substring(relativePath.length());
3352 if (!toAppend.startsWith("/"))
3353 toAppend = "/" + toAppend;
3354 generatedXML.writeText(absoluteUri + toAppend);
3355 generatedXML.writeElement(null, "href", XMLWriter.CLOSING);
3356 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
3357 generatedXML.writeText("HTTP/1.1 " + errorCode + " " + WebdavStatus.getStatusText(errorCode));
3358 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
3360 generatedXML.writeElement(null, "response", XMLWriter.CLOSING);
3364 generatedXML.writeElement(null, "multistatus", XMLWriter.CLOSING);
3366 Writer writer = resp.getWriter();
3367 writer.write(generatedXML.toString());
3372 // --------------------------------------------- WebdavResolver Inner Class
3374 * Work around for XML parsers that don't fully respect
3375 * {@link DocumentBuilderFactory#setExpandEntityReferences(boolean)}.
3376 * External references are filtered out for security reasons. See
3379 private class WebdavResolver implements EntityResolver {
3382 * A private copy of the servlet context.
3384 private ServletContext context;
3387 * Construct the resolver by passing the servlet context.
3389 * @param theContext the servlet context
3391 public WebdavResolver(ServletContext theContext) {
3392 context = theContext;
3395 public InputSource resolveEntity(String publicId, String systemId) {
3396 context.log("The request included a reference to an external entity with PublicID " + publicId + " and SystemID " + systemId + " which was ignored");
3397 return new InputSource(new StringReader("Ignored external entity"));
3402 * Returns the user making the request. This is the user whose credentials
3403 * were supplied in the authorization header.
3405 * @param req the HTTP request
3406 * @return the user making the request
3408 protected User getUser(HttpServletRequest req) {
3409 return (User) req.getAttribute(USER_ATTRIBUTE);
3413 * Retrieves the user who owns the requested namespace, as specified in the
3416 * @param req the HTTP request
3417 * @return the owner of the namespace
3419 protected User getOwner(HttpServletRequest req) {
3420 return (User) req.getAttribute(OWNER_ATTRIBUTE);