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;
35 import java.io.BufferedInputStream;
36 import java.io.ByteArrayInputStream;
37 import java.io.ByteArrayOutputStream;
39 import java.io.FileInputStream;
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.io.InputStreamReader;
43 import java.io.OutputStreamWriter;
44 import java.io.PrintWriter;
45 import java.io.RandomAccessFile;
46 import java.io.Reader;
47 import java.io.StringReader;
48 import java.io.StringWriter;
49 import java.io.UnsupportedEncodingException;
50 import java.io.Writer;
51 import java.net.URLDecoder;
52 import java.security.MessageDigest;
53 import java.security.NoSuchAlgorithmException;
54 import java.text.SimpleDateFormat;
55 import java.util.ArrayList;
56 import java.util.Date;
57 import java.util.Enumeration;
58 import java.util.Hashtable;
59 import java.util.Iterator;
60 import java.util.List;
61 import java.util.Locale;
62 import java.util.Stack;
63 import java.util.StringTokenizer;
64 import java.util.TimeZone;
65 import java.util.Vector;
67 import javax.naming.Context;
68 import javax.naming.InitialContext;
69 import javax.naming.NamingException;
70 import javax.rmi.PortableRemoteObject;
71 import javax.servlet.ServletContext;
72 import javax.servlet.ServletException;
73 import javax.servlet.ServletOutputStream;
74 import javax.servlet.UnavailableException;
75 import javax.servlet.http.HttpServlet;
76 import javax.servlet.http.HttpServletRequest;
77 import javax.servlet.http.HttpServletResponse;
78 import javax.xml.parsers.DocumentBuilder;
79 import javax.xml.parsers.DocumentBuilderFactory;
80 import javax.xml.parsers.ParserConfigurationException;
82 import org.apache.commons.httpclient.HttpStatus;
83 import org.apache.commons.logging.Log;
84 import org.apache.commons.logging.LogFactory;
85 import org.w3c.dom.Document;
86 import org.w3c.dom.Element;
87 import org.w3c.dom.Node;
88 import org.w3c.dom.NodeList;
89 import org.xml.sax.EntityResolver;
90 import org.xml.sax.InputSource;
91 import org.xml.sax.SAXException;
94 * The implementation of the WebDAV service.
98 public class Webdav extends HttpServlet {
101 * The request attribute containing the user who owns the requested
104 protected static final String OWNER_ATTRIBUTE = "owner";
107 * The request attribute containing the user making the request.
109 protected static final String USER_ATTRIBUTE = "user";
114 private static Log logger = LogFactory.getLog(Webdav.class);
119 protected static final String METHOD_GET = "GET";
124 protected static final String METHOD_POST = "POST";
129 protected static final String METHOD_PUT = "PUT";
134 protected static final String METHOD_DELETE = "DELETE";
139 protected static final String METHOD_HEAD = "HEAD";
144 private static final String METHOD_OPTIONS = "OPTIONS";
149 private static final String METHOD_PROPFIND = "PROPFIND";
154 private static final String METHOD_PROPPATCH = "PROPPATCH";
159 private static final String METHOD_MKCOL = "MKCOL";
164 private static final String METHOD_COPY = "COPY";
169 private static final String METHOD_MOVE = "MOVE";
174 private static final String METHOD_LOCK = "LOCK";
179 private static final String METHOD_UNLOCK = "UNLOCK";
182 * Default depth is infinite.
184 static final int INFINITY = 3; // To limit tree browsing a bit
187 * PROPFIND - Specify a property mask.
189 private static final int FIND_BY_PROPERTY = 0;
192 * PROPFIND - Display all properties.
194 private static final int FIND_ALL_PROP = 1;
197 * PROPFIND - Return property names.
199 private static final int FIND_PROPERTY_NAMES = 2;
204 private static final String DEFAULT_NAMESPACE = "DAV:";
209 private static final int LOCK_CREATION = 0;
214 private static final int LOCK_REFRESH = 1;
217 * Default lock timeout value.
219 private static final int DEFAULT_TIMEOUT = 3600;
222 * Maximum lock timeout.
224 private static final int MAX_TIMEOUT = 604800;
227 * Size of file transfer buffer in bytes.
229 private static final int BUFFER_SIZE = 4096;
232 * The output buffer size to use when serving resources.
234 protected int output = 2048;
237 * The input buffer size to use when serving resources.
239 private int input = 2048;
242 * MIME multipart separation string
244 protected static final String mimeSeparation = "GSS_MIME_BOUNDARY";
247 * Simple date format for the creation date ISO representation (partial).
249 private static final SimpleDateFormat creationDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
254 private static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
257 * Array containing the safe characters set.
259 private static URLEncoder urlEncoder;
262 * File encoding to be used when reading static files. If none is specified
263 * the platform default is used.
265 private String fileEncoding = null;
268 * The style sheet for displaying the directory listings.
270 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;}";
273 * Secret information used to generate reasonably secure lock ids.
275 private String secret = "gss-webdav";
281 protected static ArrayList FULL = new ArrayList();
284 * MD5 message digest provider.
286 protected static MessageDigest md5Helper;
289 * The MD5 helper object for this class.
291 protected static final MD5Encoder md5Encoder = new MD5Encoder();
294 * GMT timezone - all HTTP dates are on GMT
297 creationDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
298 urlEncoder = new URLEncoder();
299 urlEncoder.addSafeCharacter('-');
300 urlEncoder.addSafeCharacter('_');
301 urlEncoder.addSafeCharacter('.');
302 urlEncoder.addSafeCharacter('*');
303 urlEncoder.addSafeCharacter('/');
307 public void init() throws ServletException {
308 if (getServletConfig().getInitParameter("input") != null)
309 input = Integer.parseInt(getServletConfig().getInitParameter("input"));
311 if (getServletConfig().getInitParameter("output") != null)
312 output = Integer.parseInt(getServletConfig().getInitParameter("output"));
314 fileEncoding = getServletConfig().getInitParameter("fileEncoding");
316 // Sanity check on the specified buffer sizes
321 if (logger.isDebugEnabled())
322 logger.debug("Input buffer size=" + input + ", output buffer size=" + output);
324 if (getServletConfig().getInitParameter("secret") != null)
325 secret = getServletConfig().getInitParameter("secret");
327 // Load the MD5 helper used to calculate signatures.
329 md5Helper = MessageDigest.getInstance("MD5");
330 } catch (NoSuchAlgorithmException e) {
331 throw new UnavailableException("No MD5");
336 * A helper method that retrieves a reference to the ExternalAPI bean and
337 * stores it for future use.
339 * @return an ExternalAPI instance
340 * @throws RpcException in case an error occurs
342 protected ExternalAPI getService() throws RpcException {
344 final Context ctx = new InitialContext();
345 final Object ref = ctx.lookup(getConfiguration().getString("externalApiPath"));
346 return (ExternalAPI) PortableRemoteObject.narrow(ref, ExternalAPI.class);
347 } catch (final NamingException e) {
348 logger.error("Unable to retrieve the ExternalAPI EJB", e);
349 throw new RpcException("An error occurred while contacting the naming service");
354 public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
355 String method = request.getMethod();
357 if (logger.isDebugEnabled()) {
358 String path = request.getPathInfo();
360 path = request.getServletPath();
361 if (path == null || path.equals(""))
363 logger.debug("[" + method + "] " + path);
368 if (request.getUserPrincipal() != null) { // Let unauthenticated
369 // OPTIONS go through;
370 // all others will be
372 // authentication anyway
373 // before we get here.
374 user = getService().findUser(request.getUserPrincipal().getName());
376 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
380 request.setAttribute(USER_ATTRIBUTE, user);
381 request.setAttribute(OWNER_ATTRIBUTE, user);
382 } catch (RpcException e) {
383 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
386 if (method.equals(METHOD_GET))
387 doGet(request, response);
388 else if (method.equals(METHOD_POST))
389 doPost(request, response);
390 else if (method.equals(METHOD_PUT))
391 doPut(request, response);
392 else if (method.equals(METHOD_DELETE))
393 doDelete(request, response);
394 else if (method.equals(METHOD_HEAD))
395 doHead(request, response);
396 else if (method.equals(METHOD_PROPFIND))
397 doPropfind(request, response);
398 else if (method.equals(METHOD_PROPPATCH))
399 doProppatch(request, response);
400 else if (method.equals(METHOD_MKCOL))
401 doMkcol(request, response);
402 else if (method.equals(METHOD_COPY))
403 doCopy(request, response);
404 else if (method.equals(METHOD_MOVE))
405 doMove(request, response);
406 else if (method.equals(METHOD_LOCK))
407 doLock(request, response);
408 else if (method.equals(METHOD_UNLOCK))
409 doUnlock(request, response);
410 else if (method.equals(METHOD_OPTIONS))
411 doOptions(request, response);
413 // DefaultServlet processing for TRACE, etc.
414 super.service(request, response);
418 protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws IOException {
419 resp.addHeader("DAV", "1,2");
420 StringBuffer methodsAllowed = new StringBuffer();
422 methodsAllowed = determineMethodsAllowed(req);
423 } catch (RpcException e) {
424 resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
427 resp.addHeader("Allow", methodsAllowed.toString());
428 resp.addHeader("MS-Author-Via", "DAV");
432 * Implement the PROPFIND method.
434 * @param req the HTTP request
435 * @param resp the HTTP response
436 * @throws ServletException
437 * @throws IOException
439 private void doPropfind(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
440 String path = getRelativePath(req);
441 if (path.endsWith("/") && !path.equals("/"))
442 path = path.substring(0, path.length() - 1);
444 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
445 resp.sendError(WebdavStatus.SC_FORBIDDEN);
449 // Properties which are to be displayed.
450 Vector<String> properties = null;
452 int depth = INFINITY;
454 int type = FIND_ALL_PROP;
456 String depthStr = req.getHeader("Depth");
458 if (depthStr == null)
460 else if (depthStr.equals("0"))
462 else if (depthStr.equals("1"))
464 else if (depthStr.equals("infinity"))
467 Node propNode = null;
469 if (req.getInputStream().available() > 0) {
470 DocumentBuilder documentBuilder = getDocumentBuilder();
473 Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
475 // Get the root element of the document
476 Element rootElement = document.getDocumentElement();
477 NodeList childList = rootElement.getChildNodes();
479 for (int i = 0; i < childList.getLength(); i++) {
480 Node currentNode = childList.item(i);
481 switch (currentNode.getNodeType()) {
484 case Node.ELEMENT_NODE:
485 if (currentNode.getNodeName().endsWith("prop")) {
486 type = FIND_BY_PROPERTY;
487 propNode = currentNode;
489 if (currentNode.getNodeName().endsWith("propname"))
490 type = FIND_PROPERTY_NAMES;
491 if (currentNode.getNodeName().endsWith("allprop"))
492 type = FIND_ALL_PROP;
496 } catch (SAXException e) {
497 // Something went wrong - use the defaults.
498 if (logger.isDebugEnabled())
499 logger.debug(e.getMessage());
500 } catch (IOException e) {
501 // Something went wrong - use the defaults.
502 if (logger.isDebugEnabled())
503 logger.debug(e.getMessage());
507 if (type == FIND_BY_PROPERTY) {
508 properties = new Vector<String>();
509 NodeList childList = propNode.getChildNodes();
511 for (int i = 0; i < childList.getLength(); i++) {
512 Node currentNode = childList.item(i);
513 switch (currentNode.getNodeType()) {
516 case Node.ELEMENT_NODE:
517 String nodeName = currentNode.getNodeName();
518 String propertyName = null;
519 if (nodeName.indexOf(':') != -1)
520 propertyName = nodeName.substring(nodeName.indexOf(':') + 1);
522 propertyName = nodeName;
523 // href is a live property which is handled differently
524 properties.addElement(propertyName);
529 User user = getUser(req);
530 boolean exists = true;
531 Object object = null;
533 object = getService().getResourceAtPath(user.getId(), path, true);
534 } catch (ObjectNotFoundException e) {
536 } catch (RpcException e) {
537 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
541 resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
544 resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
545 resp.setContentType("text/xml; charset=UTF-8");
546 // Create multistatus object
547 XMLWriter generatedXML = new XMLWriter(resp.getWriter());
548 generatedXML.writeXMLHeader();
549 generatedXML.writeElement(null, "multistatus" + generateNamespaceDeclarations(), XMLWriter.OPENING);
551 parseProperties(req, generatedXML, path, type, properties, object);
553 // The stack always contains the object of the current level
554 Stack<String> stack = new Stack<String>();
557 // Stack of the objects one level below
558 Stack<String> stackBelow = new Stack<String>();
559 while (!stack.isEmpty() && depth >= 0) {
560 String currentPath = stack.pop();
562 object = getService().getResourceAtPath(user.getId(), currentPath, true);
563 } catch (ObjectNotFoundException e) {
565 } catch (RpcException e) {
566 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
569 parseProperties(req, generatedXML, currentPath, type, properties, object);
570 if (object instanceof FolderDTO && depth > 0) {
571 FolderDTO folder = (FolderDTO) object;
572 // Retrieve the subfolders.
573 List subfolders = folder.getSubfolders();
574 Iterator iter = subfolders.iterator();
575 while (iter.hasNext()) {
576 FolderDTO f = (FolderDTO) iter.next();
577 String newPath = currentPath;
578 if (!newPath.endsWith("/"))
580 newPath += f.getName();
581 stackBelow.push(newPath);
583 // Retrieve the files.
584 List<FileHeaderDTO> files;
586 files = getService().getFiles(user.getId(), folder.getId(), true);
587 } catch (ObjectNotFoundException e) {
588 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
590 } catch (InsufficientPermissionsException e) {
591 resp.sendError(HttpServletResponse.SC_FORBIDDEN, path);
593 } catch (RpcException e) {
594 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
597 for (FileHeaderDTO file : files) {
598 String newPath = currentPath;
599 if (!newPath.endsWith("/"))
601 newPath += file.getName();
602 stackBelow.push(newPath);
605 if (stack.isEmpty()) {
608 stackBelow = new Stack<String>();
610 generatedXML.sendData();
613 generatedXML.writeElement(null, "multistatus", XMLWriter.CLOSING);
614 generatedXML.sendData();
620 * @param req the HTTP request
621 * @param resp the HTTP response
622 * @throws IOException if an error occurs while sending the response
624 private void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
626 resp.sendError(WebdavStatus.SC_LOCKED);
629 resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
633 * @see javax.servlet.http.HttpServlet#doDelete(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
636 protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
638 resp.sendError(WebdavStatus.SC_LOCKED);
641 deleteResource(req, resp);
645 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
646 // Serve the requested resource, including the data content
648 serveResource(req, resp, true);
649 } catch (ObjectNotFoundException e) {
650 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
652 } catch (InsufficientPermissionsException e) {
653 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
655 } catch (RpcException e) {
656 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
662 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
663 resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
667 protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
669 resp.sendError(WebdavStatus.SC_LOCKED);
673 User user = getUser(req);
674 String path = getRelativePath(req);
675 boolean exists = true;
676 Object resource = null;
677 FileHeaderDTO file = null;
679 resource = getService().getResourceAtPath(user.getId(), path, true);
680 } catch (ObjectNotFoundException e) {
682 } catch (RpcException e) {
683 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
688 if (resource instanceof FileHeaderDTO)
689 file = (FileHeaderDTO) resource;
691 resp.sendError(HttpServletResponse.SC_CONFLICT);
694 boolean result = true;
696 // Temporary content file used to support partial PUT.
697 File contentFile = null;
699 Range range = parseContentRange(req, resp);
701 InputStream resourceInputStream = null;
703 // Append data specified in ranges to existing content for this
704 // resource - create a temporary file on the local filesystem to
705 // perform this operation.
706 // Assume just one range is specified for now
709 contentFile = executePartialPut(req, range, path);
710 } catch (RpcException e) {
711 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
713 } catch (ObjectNotFoundException e) {
714 resp.sendError(HttpServletResponse.SC_CONFLICT);
716 } catch (InsufficientPermissionsException e) {
717 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
720 resourceInputStream = new FileInputStream(contentFile);
722 resourceInputStream = req.getInputStream();
725 FolderDTO folder = null;
726 Object parent = getService().getResourceAtPath(user.getId(), getParentPath(path), true);
727 if (!(parent instanceof FolderDTO)) {
728 resp.sendError(HttpServletResponse.SC_CONFLICT);
731 folder = (FolderDTO) parent;
732 String name = getLastElement(path);
733 String mimeType = getServletContext().getMimeType(name);
734 File uploadedFile = null;
736 uploadedFile = getService().uploadFile(resourceInputStream, user.getId());
737 } catch (IOException ex) {
738 throw new GSSIOException(ex, false);
740 // FIXME: Add attributes
741 FileHeaderDTO fileDTO = null;
743 fileDTO = getService().updateFileContents(user.getId(), file.getId(), mimeType, uploadedFile.length(), uploadedFile.getAbsolutePath());
745 fileDTO = getService().createFile(user.getId(), folder.getId(), name, mimeType, uploadedFile.length(), uploadedFile.getAbsolutePath());
746 getService().updateAccounting(user, new Date(), fileDTO.getFileSize());
747 } catch (ObjectNotFoundException e) {
749 } catch (InsufficientPermissionsException e) {
750 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
752 } catch (QuotaExceededException e) {
753 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
755 } catch (GSSIOException e) {
756 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
758 } catch (RpcException e) {
759 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
761 } catch (DuplicateNameException e) {
762 resp.sendError(HttpServletResponse.SC_CONFLICT);
768 resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
770 resp.setStatus(HttpServletResponse.SC_CREATED);
772 resp.sendError(HttpServletResponse.SC_CONFLICT);
777 protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
778 // Serve the requested resource, without the data content
780 serveResource(req, resp, false);
781 } catch (ObjectNotFoundException e) {
782 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
784 } catch (InsufficientPermissionsException e) {
785 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
787 } catch (RpcException e) {
788 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
796 * @param req the HTTP request
797 * @param resp the HTTP response
798 * @throws IOException if an error occurs while sending the response
800 private void doUnlock(@SuppressWarnings("unused") HttpServletRequest req, HttpServletResponse resp) throws IOException {
801 resp.setStatus(WebdavStatus.SC_NO_CONTENT);
807 * @param req the HTTP request
808 * @param resp the HTTP response
809 * @throws IOException if an error occurs while sending the response
810 * @throws ServletException
812 private void doLock(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
813 LockInfo lock = new LockInfo();
814 // Parsing lock request
816 // Parsing depth header
817 String depthStr = req.getHeader("Depth");
818 if (depthStr == null)
819 lock.depth = INFINITY;
820 else if (depthStr.equals("0"))
823 lock.depth = INFINITY;
825 // Parsing timeout header
826 int lockDuration = DEFAULT_TIMEOUT;
827 String lockDurationStr = req.getHeader("Timeout");
828 if (lockDurationStr == null)
829 lockDuration = DEFAULT_TIMEOUT;
831 int commaPos = lockDurationStr.indexOf(",");
832 // If multiple timeouts, just use the first
834 lockDurationStr = lockDurationStr.substring(0, commaPos);
835 if (lockDurationStr.startsWith("Second-"))
836 lockDuration = new Integer(lockDurationStr.substring(7)).intValue();
837 else if (lockDurationStr.equalsIgnoreCase("infinity"))
838 lockDuration = MAX_TIMEOUT;
841 lockDuration = new Integer(lockDurationStr).intValue();
842 } catch (NumberFormatException e) {
843 lockDuration = MAX_TIMEOUT;
845 if (lockDuration == 0)
846 lockDuration = DEFAULT_TIMEOUT;
847 if (lockDuration > MAX_TIMEOUT)
848 lockDuration = MAX_TIMEOUT;
850 lock.expiresAt = System.currentTimeMillis() + lockDuration * 1000;
852 int lockRequestType = LOCK_CREATION;
853 Node lockInfoNode = null;
854 DocumentBuilder documentBuilder = getDocumentBuilder();
857 Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
858 // Get the root element of the document
859 Element rootElement = document.getDocumentElement();
860 lockInfoNode = rootElement;
861 } catch (IOException e) {
862 lockRequestType = LOCK_REFRESH;
863 } catch (SAXException e) {
864 lockRequestType = LOCK_REFRESH;
867 if (lockInfoNode != null) {
868 // Reading lock information
869 NodeList childList = lockInfoNode.getChildNodes();
870 StringWriter strWriter = null;
871 DOMWriter domWriter = null;
873 Node lockScopeNode = null;
874 Node lockTypeNode = null;
875 Node lockOwnerNode = null;
877 for (int i = 0; i < childList.getLength(); i++) {
878 Node currentNode = childList.item(i);
879 switch (currentNode.getNodeType()) {
882 case Node.ELEMENT_NODE:
883 String nodeName = currentNode.getNodeName();
884 if (nodeName.endsWith("lockscope"))
885 lockScopeNode = currentNode;
886 if (nodeName.endsWith("locktype"))
887 lockTypeNode = currentNode;
888 if (nodeName.endsWith("owner"))
889 lockOwnerNode = currentNode;
894 if (lockScopeNode != null) {
895 childList = lockScopeNode.getChildNodes();
896 for (int i = 0; i < childList.getLength(); i++) {
897 Node currentNode = childList.item(i);
898 switch (currentNode.getNodeType()) {
901 case Node.ELEMENT_NODE:
902 String tempScope = currentNode.getNodeName();
903 if (tempScope.indexOf(':') != -1)
904 lock.scope = tempScope.substring(tempScope.indexOf(':') + 1);
906 lock.scope = tempScope;
910 if (lock.scope == null)
912 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
915 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
917 if (lockTypeNode != null) {
918 childList = lockTypeNode.getChildNodes();
919 for (int i = 0; i < childList.getLength(); i++) {
920 Node currentNode = childList.item(i);
921 switch (currentNode.getNodeType()) {
924 case Node.ELEMENT_NODE:
925 String tempType = currentNode.getNodeName();
926 if (tempType.indexOf(':') != -1)
927 lock.type = tempType.substring(tempType.indexOf(':') + 1);
929 lock.type = tempType;
934 if (lock.type == null)
936 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
939 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
941 if (lockOwnerNode != null) {
942 childList = lockOwnerNode.getChildNodes();
943 for (int i = 0; i < childList.getLength(); i++) {
944 Node currentNode = childList.item(i);
945 switch (currentNode.getNodeType()) {
947 lock.owner += currentNode.getNodeValue();
949 case Node.ELEMENT_NODE:
950 strWriter = new StringWriter();
951 domWriter = new DOMWriter(strWriter, true);
952 domWriter.setQualifiedNames(false);
953 domWriter.print(currentNode);
954 lock.owner += strWriter.toString();
959 if (lock.owner == null)
961 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
963 lock.owner = new String();
966 String path = getRelativePath(req);
968 User user = getUser(req);
969 boolean exists = true;
970 Object object = null;
972 object = getService().getResourceAtPath(user.getId(), path, true);
973 } catch (ObjectNotFoundException e) {
975 } catch (RpcException e) {
976 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
980 if (lockRequestType == LOCK_CREATION) {
981 // Generating lock id
982 String lockTokenStr = req.getServletPath() + "-" + lock.type + "-" + lock.scope + "-" + req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.tokens + "-" + lock.expiresAt + "-" + System.currentTimeMillis() + "-" + secret;
983 String lockToken = md5Encoder.encode(md5Helper.digest(lockTokenStr.getBytes()));
985 if (exists && object instanceof FolderDTO && lock.depth == INFINITY)
986 // Locking a collection (and all its member resources)
987 lock.tokens.addElement(lockToken);
989 // Locking a single resource
990 lock.tokens.addElement(lockToken);
991 // Add the Lock-Token header as by RFC 2518 8.10.1
992 // - only do this for newly created locks
993 resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">");
998 if (lockRequestType == LOCK_REFRESH) {
1002 // Set the status, then generate the XML response containing
1003 // the lock information.
1004 XMLWriter generatedXML = new XMLWriter();
1005 generatedXML.writeXMLHeader();
1006 generatedXML.writeElement(null, "prop" + generateNamespaceDeclarations(), XMLWriter.OPENING);
1007 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.OPENING);
1008 lock.toXML(generatedXML);
1009 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.CLOSING);
1010 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1012 resp.setStatus(WebdavStatus.SC_OK);
1013 resp.setContentType("text/xml; charset=UTF-8");
1014 Writer writer = resp.getWriter();
1015 writer.write(generatedXML.toString());
1022 * @param req the HTTP request
1023 * @param resp the HTTP response
1024 * @throws IOException if an error occurs while sending the response
1025 * @throws ServletException
1027 private void doMove(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1028 if (isLocked(req)) {
1029 resp.sendError(WebdavStatus.SC_LOCKED);
1033 String path = getRelativePath(req);
1035 if (copyResource(req, resp))
1036 deleteResource(path, req, resp, false);
1042 * @param req the HTTP request
1043 * @param resp the HTTP response
1044 * @throws IOException if an error occurs while sending the response
1045 * @throws ServletException
1047 private void doCopy(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1048 copyResource(req, resp);
1054 * @param req the HTTP request
1055 * @param resp the HTTP response
1056 * @throws IOException if an error occurs while sending the response
1057 * @throws ServletException
1059 private void doMkcol(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1060 if (isLocked(req)) {
1061 resp.sendError(WebdavStatus.SC_LOCKED);
1064 String path = getRelativePath(req);
1065 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
1066 resp.sendError(WebdavStatus.SC_FORBIDDEN);
1070 User user = getUser(req);
1071 boolean exists = true;
1073 getService().getResourceAtPath(user.getId(), path, true);
1074 } catch (ObjectNotFoundException e) {
1076 } catch (RpcException e) {
1077 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1081 // Can't create a collection if a resource already exists at the given
1084 // Get allowed methods.
1085 StringBuffer methodsAllowed;
1087 methodsAllowed = determineMethodsAllowed(req);
1088 } catch (RpcException e) {
1089 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1092 resp.addHeader("Allow", methodsAllowed.toString());
1093 resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
1097 if (req.getInputStream().available() > 0) {
1098 DocumentBuilder documentBuilder = getDocumentBuilder();
1100 Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
1101 // TODO : Process this request body
1102 resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED);
1104 } catch (SAXException saxe) {
1105 // Parse error - assume invalid content
1106 resp.sendError(WebdavStatus.SC_BAD_REQUEST);
1113 parent = getService().getResourceAtPath(user.getId(), getParentPath(path), true);
1114 } catch (ObjectNotFoundException e1) {
1115 resp.sendError(WebdavStatus.SC_CONFLICT, WebdavStatus.getStatusText(WebdavStatus.SC_CONFLICT));
1117 } catch (RpcException e1) {
1118 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1122 if (parent instanceof FolderDTO) {
1123 FolderDTO folder = (FolderDTO) parent;
1124 getService().createFolder(user.getId(), folder.getId(), getLastElement(path));
1126 resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1129 } catch (DuplicateNameException e) {
1130 // XXX If the existing name is a folder we should be returning
1131 // SC_METHOD_NOT_ALLOWED, or even better, just do the createFolder
1132 // without checking first and then deal with the exceptions.
1133 resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1135 } catch (InsufficientPermissionsException e) {
1136 resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1138 } catch (ObjectNotFoundException e) {
1139 resp.sendError(WebdavStatus.SC_CONFLICT, WebdavStatus.getStatusText(WebdavStatus.SC_CONFLICT));
1141 } catch (RpcException e) {
1142 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1145 resp.setStatus(WebdavStatus.SC_CREATED);
1149 * For a provided path, remove the last element and return the rest, that is
1150 * the path of the parent folder.
1152 * @param path the specified path
1153 * @return the path of the parent folder
1154 * @throws ObjectNotFoundException if the provided string contains no path
1157 protected String getParentPath(String path) throws ObjectNotFoundException {
1158 int lastDelimiter = path.lastIndexOf('/');
1159 if (lastDelimiter == 0)
1161 if (lastDelimiter == -1)
1163 throw new ObjectNotFoundException("There is no parent in the path: " + path);
1164 else if (lastDelimiter < path.length() - 1)
1165 // Return the part before the delimiter.
1166 return path.substring(0, lastDelimiter);
1168 // Remove the trailing delimiter and then recurse.
1169 String strippedTrail = path.substring(0, lastDelimiter);
1170 return getParentPath(strippedTrail);
1175 * Get the last element in a path that denotes the file or folder name.
1177 * @param path the provided path
1178 * @return the last element in the path
1180 protected String getLastElement(String path) {
1181 int lastDelimiter = path.lastIndexOf('/');
1182 if (lastDelimiter == -1)
1185 else if (lastDelimiter < path.length() - 1)
1186 // Return the part after the delimiter.
1187 return path.substring(lastDelimiter + 1);
1189 // Remove the trailing delimiter and then recurse.
1190 String strippedTrail = path.substring(0, lastDelimiter);
1191 return getLastElement(strippedTrail);
1196 * Only use the PathInfo for determining the requested path. If the
1197 * ServletPath is non-null, it will be because the WebDAV servlet has been
1198 * mapped to a URL other than /* to configure editing at different URL than
1201 * @param request the servlet request we are processing
1202 * @return the relative path
1203 * @throws UnsupportedEncodingException
1205 protected String getRelativePath(HttpServletRequest request) {
1206 // Remove the servlet path from the request URI.
1207 String p = request.getRequestURI();
1208 String servletPath = request.getContextPath() + request.getServletPath();
1209 String result = p.substring(servletPath.length());
1211 result = URLDecoder.decode(result, "UTF-8");
1212 } catch (UnsupportedEncodingException e) {
1214 if (result == null || result.equals(""))
1221 * Return JAXP document builder instance.
1223 * @return the DocumentBuilder
1224 * @throws ServletException
1226 private DocumentBuilder getDocumentBuilder() throws ServletException {
1227 DocumentBuilder documentBuilder = null;
1228 DocumentBuilderFactory documentBuilderFactory = null;
1230 documentBuilderFactory = DocumentBuilderFactory.newInstance();
1231 documentBuilderFactory.setNamespaceAware(true);
1232 documentBuilderFactory.setExpandEntityReferences(false);
1233 documentBuilder = documentBuilderFactory.newDocumentBuilder();
1234 documentBuilder.setEntityResolver(new WebdavResolver(getServletContext()));
1235 } catch (ParserConfigurationException e) {
1236 throw new ServletException("Error while creating a document builder");
1238 return documentBuilder;
1242 * Generate the namespace declarations.
1244 * @return the namespace declarations
1246 private String generateNamespaceDeclarations() {
1247 return " xmlns=\"" + DEFAULT_NAMESPACE + "\"";
1251 * Propfind helper method. Dispays the properties of a lock-null resource.
1253 * @param req the HTTP request
1254 * @param resources Resources object associated with this context
1255 * @param generatedXML XML response to the Propfind request
1256 * @param path Path of the current resource
1257 * @param type Propfind type
1258 * @param propertiesVector If the propfind type is find properties by name,
1259 * then this Vector contains those properties
1261 @SuppressWarnings("unused")
1262 private void parseLockNullProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, Vector propertiesVector) {
1267 * Propfind helper method.
1269 * @param req The servlet request
1270 * @param resources Resources object associated with this context
1271 * @param generatedXML XML response to the Propfind request
1272 * @param path Path of the current resource
1273 * @param type Propfind type
1274 * @param propertiesVector If the propfind type is find properties by name,
1275 * then this Vector contains those properties
1276 * @param resource the resource object
1278 private void parseProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, Vector<String> propertiesVector, Object resource) {
1280 // Exclude any resource in the /WEB-INF and /META-INF subdirectories
1281 // (the "toUpperCase()" avoids problems on Windows systems)
1282 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF"))
1285 FolderDTO folder = null;
1286 FileHeaderDTO file = null;
1287 if (resource instanceof FolderDTO)
1288 folder = (FolderDTO) resource;
1290 file = (FileHeaderDTO) resource;
1291 // Retrieve the creation date.
1294 creation = folder.getAuditInfo().getCreationDate().getTime();
1296 creation = file.getAuditInfo().getCreationDate().getTime();
1297 // Retrieve the modification date.
1298 long modification = 0;
1300 modification = folder.getAuditInfo().getCreationDate().getTime();
1302 modification = file.getAuditInfo().getCreationDate().getTime();
1304 generatedXML.writeElement(null, "response", XMLWriter.OPENING);
1305 String status = new String("HTTP/1.1 " + WebdavStatus.SC_OK + " " + WebdavStatus.getStatusText(WebdavStatus.SC_OK));
1307 // Generating href element
1308 generatedXML.writeElement(null, "href", XMLWriter.OPENING);
1310 String href = req.getContextPath() + req.getServletPath();
1311 if (href.endsWith("/") && path.startsWith("/"))
1312 href += path.substring(1);
1315 if (folder != null && !href.endsWith("/"))
1318 generatedXML.writeText(rewriteUrl(href));
1320 generatedXML.writeElement(null, "href", XMLWriter.CLOSING);
1322 String resourceName = path;
1323 int lastSlash = path.lastIndexOf('/');
1324 if (lastSlash != -1)
1325 resourceName = resourceName.substring(lastSlash + 1);
1331 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1332 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1334 generatedXML.writeProperty(null, "creationdate", getISOCreationDate(creation));
1335 generatedXML.writeElement(null, "displayname", XMLWriter.OPENING);
1336 generatedXML.writeData(resourceName);
1337 generatedXML.writeElement(null, "displayname", XMLWriter.CLOSING);
1339 generatedXML.writeProperty(null, "getlastmodified", FastHttpDateFormat.formatDate(modification, null));
1340 generatedXML.writeProperty(null, "getcontentlength", String.valueOf(file.getFileSize()));
1341 String contentType = file.getMimeType();
1342 if (contentType != null)
1343 generatedXML.writeProperty(null, "getcontenttype", contentType);
1344 generatedXML.writeProperty(null, "getetag", getETag(file, null));
1345 generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1347 generatedXML.writeElement(null, "resourcetype", XMLWriter.OPENING);
1348 generatedXML.writeElement(null, "collection", XMLWriter.NO_CONTENT);
1349 generatedXML.writeElement(null, "resourcetype", XMLWriter.CLOSING);
1352 generatedXML.writeProperty(null, "source", "");
1354 String supportedLocks = "<lockentry>" + "<lockscope><exclusive/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>" + "<lockentry>" + "<lockscope><shared/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>";
1355 generatedXML.writeElement(null, "supportedlock", XMLWriter.OPENING);
1356 generatedXML.writeText(supportedLocks);
1357 generatedXML.writeElement(null, "supportedlock", XMLWriter.CLOSING);
1359 generateLockDiscovery(path, generatedXML);
1361 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1362 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1363 generatedXML.writeText(status);
1364 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1365 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1369 case FIND_PROPERTY_NAMES:
1371 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1372 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1374 generatedXML.writeElement(null, "creationdate", XMLWriter.NO_CONTENT);
1375 generatedXML.writeElement(null, "displayname", XMLWriter.NO_CONTENT);
1377 generatedXML.writeElement(null, "getcontentlanguage", XMLWriter.NO_CONTENT);
1378 generatedXML.writeElement(null, "getcontentlength", XMLWriter.NO_CONTENT);
1379 generatedXML.writeElement(null, "getcontenttype", XMLWriter.NO_CONTENT);
1380 generatedXML.writeElement(null, "getetag", XMLWriter.NO_CONTENT);
1381 generatedXML.writeElement(null, "getlastmodified", XMLWriter.NO_CONTENT);
1383 generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1384 generatedXML.writeElement(null, "source", XMLWriter.NO_CONTENT);
1385 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.NO_CONTENT);
1387 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1388 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1389 generatedXML.writeText(status);
1390 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1391 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1395 case FIND_BY_PROPERTY:
1397 Vector<String> propertiesNotFound = new Vector<String>();
1399 // Parse the list of properties
1401 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1402 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1404 Enumeration<String> properties = propertiesVector.elements();
1406 while (properties.hasMoreElements()) {
1408 String property = properties.nextElement();
1410 if (property.equals("creationdate"))
1411 generatedXML.writeProperty(null, "creationdate", getISOCreationDate(creation));
1412 else if (property.equals("displayname")) {
1413 generatedXML.writeElement(null, "displayname", XMLWriter.OPENING);
1414 generatedXML.writeData(resourceName);
1415 generatedXML.writeElement(null, "displayname", XMLWriter.CLOSING);
1416 } else if (property.equals("getcontentlanguage")) {
1418 propertiesNotFound.addElement(property);
1420 generatedXML.writeElement(null, "getcontentlanguage", XMLWriter.NO_CONTENT);
1421 } else if (property.equals("getcontentlength")) {
1423 propertiesNotFound.addElement(property);
1425 generatedXML.writeProperty(null, "getcontentlength", String.valueOf(file.getFileSize()));
1426 } else if (property.equals("getcontenttype")) {
1428 propertiesNotFound.addElement(property);
1430 // XXX Once we properly store the MIME type in the
1432 // retrieve it from there.
1433 generatedXML.writeProperty(null, "getcontenttype", getServletContext().getMimeType(file.getName()));
1434 } else if (property.equals("getetag")) {
1436 propertiesNotFound.addElement(property);
1438 generatedXML.writeProperty(null, "getetag", getETag(file, null));
1439 } else if (property.equals("getlastmodified")) {
1441 propertiesNotFound.addElement(property);
1443 generatedXML.writeProperty(null, "getlastmodified", FastHttpDateFormat.formatDate(modification, null));
1444 } else if (property.equals("resourcetype")) {
1445 if (folder != null) {
1446 generatedXML.writeElement(null, "resourcetype", XMLWriter.OPENING);
1447 generatedXML.writeElement(null, "collection", XMLWriter.NO_CONTENT);
1448 generatedXML.writeElement(null, "resourcetype", XMLWriter.CLOSING);
1450 generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1451 } else if (property.equals("source"))
1452 generatedXML.writeProperty(null, "source", "");
1453 else if (property.equals("supportedlock")) {
1454 supportedLocks = "<lockentry>" + "<lockscope><exclusive/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>" + "<lockentry>" + "<lockscope><shared/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>";
1455 generatedXML.writeElement(null, "supportedlock", XMLWriter.OPENING);
1456 generatedXML.writeText(supportedLocks);
1457 generatedXML.writeElement(null, "supportedlock", XMLWriter.CLOSING);
1458 } else if (property.equals("lockdiscovery")) {
1459 if (!generateLockDiscovery(path, generatedXML))
1460 propertiesNotFound.addElement(property);
1462 propertiesNotFound.addElement(property);
1465 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1466 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1467 generatedXML.writeText(status);
1468 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1469 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1471 Enumeration propertiesNotFoundList = propertiesNotFound.elements();
1473 if (propertiesNotFoundList.hasMoreElements()) {
1475 status = new String("HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + " " + WebdavStatus.getStatusText(WebdavStatus.SC_NOT_FOUND));
1477 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1478 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1480 while (propertiesNotFoundList.hasMoreElements())
1481 generatedXML.writeElement(null, (String) propertiesNotFoundList.nextElement(), XMLWriter.NO_CONTENT);
1482 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1483 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1484 generatedXML.writeText(status);
1485 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1486 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1493 generatedXML.writeElement(null, "response", XMLWriter.CLOSING);
1498 * Get the ETag associated with a file.
1500 * @param file the FileHeaderDTO object for this file
1501 * @param oldBody the old version of the file, if requested
1502 * @return a string containing the ETag
1504 protected String getETag(FileHeaderDTO file, FileBodyDTO oldBody) {
1505 if (oldBody == null)
1506 return "\"" + file.getFileSize() + "-" + file.getAuditInfo().getModificationDate().getTime() + "\"";
1507 return "\"" + oldBody.getFileSize() + "-" + oldBody.getAuditInfo().getModificationDate().getTime() + "\"";
1513 * @param path Path which has to be rewritten
1514 * @return the rewritten URL
1516 private String rewriteUrl(String path) {
1517 return urlEncoder.encode(path);
1521 * Print the lock discovery information associated with a path.
1524 * @param generatedXML XML data to which the locks info will be appended
1525 * @return true if at least one lock was displayed
1527 @SuppressWarnings("unused")
1528 private boolean generateLockDiscovery(String path, XMLWriter generatedXML) {
1533 * Get creation date in ISO format.
1535 * @param creationDate
1536 * @return the formatted date
1538 private String getISOCreationDate(long creationDate) {
1539 String dateValue = null;
1540 synchronized (creationDateFormat) {
1541 dateValue = creationDateFormat.format(new Date(creationDate));
1543 StringBuffer creationDateValue = new StringBuffer(dateValue);
1545 int offset = Calendar.getInstance().getTimeZone().getRawOffset()
1546 / 3600000; // FIXME ?
1548 creationDateValue.append("-");
1550 } else if (offset > 0) {
1551 creationDateValue.append("+");
1555 creationDateValue.append("0");
1556 creationDateValue.append(offset + ":00");
1558 creationDateValue.append("Z");
1561 return creationDateValue.toString();
1565 * Determines the methods normally allowed for the resource.
1567 * @param req the HTTP request
1568 * @return a list of the allowed methods
1569 * @throws RpcException if there is an error while communicating with the
1572 private StringBuffer determineMethodsAllowed(HttpServletRequest req) throws RpcException {
1573 StringBuffer methodsAllowed = new StringBuffer();
1574 boolean exists = true;
1575 Object object = null;
1576 User user = getUser(req);
1577 String path = getRelativePath(req);
1578 if (user == null && "/".equals(path))
1579 // Special case: OPTIONS request before authentication
1580 return new StringBuffer("OPTIONS, GET, HEAD, POST, DELETE, TRACE, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, PROPFIND, PUT");
1582 object = getService().getResourceAtPath(user.getId(), path, true);
1583 } catch (ObjectNotFoundException e) {
1588 methodsAllowed.append("OPTIONS, MKCOL, PUT, LOCK");
1589 return methodsAllowed;
1592 methodsAllowed.append("OPTIONS, GET, HEAD, POST, DELETE, TRACE");
1593 methodsAllowed.append(", PROPPATCH, COPY, MOVE, LOCK, UNLOCK");
1594 methodsAllowed.append(", PROPFIND");
1596 if (!(object instanceof FolderDTO))
1597 methodsAllowed.append(", PUT");
1599 return methodsAllowed;
1603 * Check to see if a resource is currently write locked. The method will
1604 * look at the "If" header to make sure the client has given the appropriate
1607 * @param req the HTTP request
1608 * @return boolean true if the resource is locked (and no appropriate lock
1609 * token has been found for at least one of the non-shared locks
1610 * which are present on the resource).
1612 private boolean isLocked(@SuppressWarnings("unused") HttpServletRequest req) {
1617 * Check to see if a resource is currently write locked.
1619 * @param path Path of the resource
1620 * @param ifHeader "If" HTTP header which was included in the request
1621 * @return boolean true if the resource is locked (and no appropriate lock
1622 * token has been found for at least one of the non-shared locks
1623 * which are present on the resource).
1625 private boolean isLocked(@SuppressWarnings("unused") String path, @SuppressWarnings("unused") String ifHeader) {
1630 * Parse the content-range header.
1632 * @param request The servlet request we are processing
1633 * @param response The servlet response we are creating
1635 * @throws IOException
1637 protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response) throws IOException {
1639 // Retrieving the content-range header (if any is specified
1640 String rangeHeader = request.getHeader("Content-Range");
1642 if (rangeHeader == null)
1645 // bytes is the only range unit supported
1646 if (!rangeHeader.startsWith("bytes")) {
1647 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1651 rangeHeader = rangeHeader.substring(6).trim();
1653 int dashPos = rangeHeader.indexOf('-');
1654 int slashPos = rangeHeader.indexOf('/');
1656 if (dashPos == -1) {
1657 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1661 if (slashPos == -1) {
1662 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1666 Range range = new Range();
1669 range.start = Long.parseLong(rangeHeader.substring(0, dashPos));
1670 range.end = Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos));
1671 range.length = Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length()));
1672 } catch (NumberFormatException e) {
1673 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1677 if (!range.validate()) {
1678 response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1687 * Handle a partial PUT. New content specified in request is appended to
1688 * existing content in oldRevisionContent (if present). This code does not
1689 * support simultaneous partial updates to the same resource.
1695 * @throws IOException
1696 * @throws RpcException
1697 * @throws InsufficientPermissionsException
1698 * @throws ObjectNotFoundException
1700 protected File executePartialPut(HttpServletRequest req, Range range, String path) throws IOException, RpcException, ObjectNotFoundException, InsufficientPermissionsException {
1701 // Append data specified in ranges to existing content for this
1702 // resource - create a temporary file on the local file system to
1703 // perform this operation.
1704 File tempDir = (File) getServletContext().getAttribute("javax.servlet.context.tempdir");
1705 // Convert all '/' characters to '.' in resourcePath
1706 String convertedResourcePath = path.replace('/', '.');
1707 File contentFile = new File(tempDir, convertedResourcePath);
1708 if (contentFile.createNewFile())
1709 // Clean up contentFile when Tomcat is terminated.
1710 contentFile.deleteOnExit();
1712 RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw");
1714 User user = getUser(req);
1715 User owner = getOwner(req);
1716 FileHeaderDTO oldResource = null;
1718 Object obj = getService().getResourceAtPath(owner.getId(), path, true);
1719 if (obj instanceof FileHeaderDTO)
1720 oldResource = (FileHeaderDTO) obj;
1721 } catch (ObjectNotFoundException e) {
1725 // Copy data in oldRevisionContent to contentFile
1726 if (oldResource != null) {
1727 InputStream contents = getService().getFileContents(user.getId(), oldResource.getId());
1728 BufferedInputStream bufOldRevStream = new BufferedInputStream(contents, BUFFER_SIZE);
1731 byte[] copyBuffer = new byte[BUFFER_SIZE];
1732 while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1)
1733 randAccessContentFile.write(copyBuffer, 0, numBytesRead);
1735 bufOldRevStream.close();
1738 randAccessContentFile.setLength(range.length);
1740 // Append data in request input stream to contentFile
1741 randAccessContentFile.seek(range.start);
1743 byte[] transferBuffer = new byte[BUFFER_SIZE];
1744 BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE);
1745 while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1)
1746 randAccessContentFile.write(transferBuffer, 0, numBytesRead);
1747 randAccessContentFile.close();
1748 requestBufInStream.close();
1755 * Serve the specified resource, optionally including the data content.
1757 * @param req The servlet request we are processing
1758 * @param resp The servlet response we are creating
1759 * @param content Should the content be included?
1760 * @exception IOException if an input/output error occurs
1761 * @exception ServletException if a servlet-specified error occurs
1762 * @throws RpcException
1763 * @throws InsufficientPermissionsException
1764 * @throws ObjectNotFoundException
1766 protected void serveResource(HttpServletRequest req, HttpServletResponse resp, boolean content) throws IOException, ServletException, ObjectNotFoundException, InsufficientPermissionsException, RpcException {
1768 // Identify the requested resource path
1769 String path = getRelativePath(req);
1770 if (logger.isDebugEnabled())
1772 logger.debug("Serving resource '" + path + "' headers and data");
1774 logger.debug("Serving resource '" + path + "' headers only");
1776 User user = getUser(req);
1777 boolean exists = true;
1778 Object resource = null;
1779 FileHeaderDTO file = null;
1780 FolderDTO folder = null;
1782 resource = getService().getResourceAtPath(user.getId(), path, true);
1783 } catch (ObjectNotFoundException e) {
1785 } catch (RpcException e) {
1786 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1791 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
1795 if (resource instanceof FolderDTO)
1796 folder = (FolderDTO) resource;
1798 file = (FileHeaderDTO) resource;
1800 // If the resource is not a collection, and the resource path
1801 // ends with "/" or "\", return NOT FOUND
1803 if (path.endsWith("/") || path.endsWith("\\")) {
1804 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
1808 // Check if the conditions specified in the optional If headers are
1811 // Checking If headers
1812 if (!checkIfHeaders(req, resp, file, null))
1815 // Find content type.
1816 String contentType = null;
1818 contentType = file.getMimeType();
1819 if (contentType == null) {
1820 contentType = getServletContext().getMimeType(file.getName());
1821 file.setMimeType(contentType);
1824 contentType = "text/html;charset=UTF-8";
1826 ArrayList ranges = null;
1827 long contentLength = -1L;
1830 // Accept ranges header
1831 resp.setHeader("Accept-Ranges", "bytes");
1832 // Parse range specifier
1833 ranges = parseRange(req, resp, file, null);
1835 resp.setHeader("ETag", getETag(file, null));
1836 // Last-Modified header
1837 resp.setHeader("Last-Modified", getLastModifiedHttp(file.getAuditInfo()));
1838 // Get content length
1839 contentLength = file.getFileSize();
1840 // Special case for zero length files, which would cause a
1841 // (silent) ISE when setting the output buffer size
1842 if (contentLength == 0L)
1846 ServletOutputStream ostream = null;
1847 PrintWriter writer = null;
1851 ostream = resp.getOutputStream();
1852 } catch (IllegalStateException e) {
1853 // If it fails, we try to get a Writer instead if we're
1854 // trying to serve a text file
1855 if (contentType == null || contentType.startsWith("text") || contentType.endsWith("xml"))
1856 writer = resp.getWriter();
1861 if (folder != null || (ranges == null || ranges.isEmpty()) && req.getHeader("Range") == null || ranges == FULL) {
1862 // Set the appropriate output headers
1863 if (contentType != null) {
1864 if (logger.isDebugEnabled())
1865 logger.debug("DefaultServlet.serveFile: contentType='" + contentType + "'");
1866 resp.setContentType(contentType);
1868 if (file != null && contentLength >= 0) {
1869 if (logger.isDebugEnabled())
1870 logger.debug("DefaultServlet.serveFile: contentLength=" + contentLength);
1871 if (contentLength < Integer.MAX_VALUE)
1872 resp.setContentLength((int) contentLength);
1874 // Set the content-length as String to be able to use a long
1875 resp.setHeader("content-length", "" + contentLength);
1878 InputStream renderResult = null;
1881 // Serve the directory browser
1882 renderResult = renderHtml(req.getContextPath(), path, folder, req);
1884 // Copy the input stream to our output stream (if requested)
1887 resp.setBufferSize(output);
1888 } catch (IllegalStateException e) {
1891 if (ostream != null)
1892 copy(file, renderResult, ostream, req, null);
1894 copy(file, renderResult, writer, req, null);
1895 getService().updateAccounting(user, new Date(), contentLength);
1898 if (ranges == null || ranges.isEmpty())
1900 // Partial content response.
1901 resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
1903 if (ranges.size() == 1) {
1904 Range range = (Range) ranges.get(0);
1905 resp.addHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);
1906 long length = range.end - range.start + 1;
1907 if (length < Integer.MAX_VALUE)
1908 resp.setContentLength((int) length);
1910 // Set the content-length as String to be able to use a long
1911 resp.setHeader("content-length", "" + length);
1913 if (contentType != null) {
1914 if (logger.isDebugEnabled())
1915 logger.debug("DefaultServlet.serveFile: contentType='" + contentType + "'");
1916 resp.setContentType(contentType);
1921 resp.setBufferSize(output);
1922 } catch (IllegalStateException e) {
1925 if (ostream != null)
1926 copy(file, ostream, range, req, null);
1928 copy(file, writer, range, req, null);
1929 getService().updateAccounting(user, new Date(), contentLength);
1934 resp.setContentType("multipart/byteranges; boundary=" + mimeSeparation);
1938 resp.setBufferSize(output);
1939 } catch (IllegalStateException e) {
1942 if (ostream != null)
1943 copy(file, ostream, ranges.iterator(), contentType, req, null);
1945 copy(file, writer, ranges.iterator(), contentType, req, null);
1955 * Retrieve the last modified date of a resource in HTTP format.
1957 * @param auditInfo the audit info for the specified resource
1958 * @return the last modified date in HTTP format
1960 protected String getLastModifiedHttp(AuditInfoDTO auditInfo) {
1961 Date modifiedDate = auditInfo.getModificationDate();
1962 if (modifiedDate == null)
1963 modifiedDate = auditInfo.getCreationDate();
1964 if (modifiedDate == null)
1965 modifiedDate = new Date();
1966 String lastModifiedHttp = null;
1967 synchronized (format) {
1968 lastModifiedHttp = format.format(modifiedDate);
1970 return lastModifiedHttp;
1974 * Parse the range header.
1976 * @param request The servlet request we are processing
1977 * @param response The servlet response we are creating
1979 * @param oldBody the old version of the file, if requested
1980 * @return Vector of ranges
1981 * @throws IOException
1983 protected ArrayList parseRange(HttpServletRequest request, HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
1984 // Checking If-Range
1985 String headerValue = request.getHeader("If-Range");
1986 if (headerValue != null) {
1987 long headerValueTime = -1L;
1989 headerValueTime = request.getDateHeader("If-Range");
1990 } catch (IllegalArgumentException e) {
1994 String eTag = getETag(file, oldBody);
1995 long lastModified = oldBody == null ?
1996 file.getAuditInfo().getModificationDate().getTime() :
1997 oldBody.getAuditInfo().getModificationDate().getTime();
1999 if (headerValueTime == -1L) {
2000 // If the ETag the client gave does not match the entity
2001 // etag, then the entire entity is returned.
2002 if (!eTag.equals(headerValue.trim()))
2005 // If the timestamp of the entity the client got is older than
2006 // the last modification date of the entity, the entire entity
2008 if (lastModified > headerValueTime + 1000)
2012 long fileLength = oldBody == null ? file.getFileSize() : oldBody.getFileSize();
2013 if (fileLength == 0)
2016 // Retrieving the range header (if any is specified).
2017 String rangeHeader = request.getHeader("Range");
2019 if (rangeHeader == null)
2021 // bytes is the only range unit supported (and I don't see the point
2022 // of adding new ones).
2023 if (!rangeHeader.startsWith("bytes")) {
2024 response.addHeader("Content-Range", "bytes */" + fileLength);
2025 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2029 rangeHeader = rangeHeader.substring(6);
2031 // Vector that will contain all the ranges which are successfully
2033 ArrayList<Range> result = new ArrayList<Range>();
2034 StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
2035 // Parsing the range list
2036 while (commaTokenizer.hasMoreTokens()) {
2037 String rangeDefinition = commaTokenizer.nextToken().trim();
2039 Range currentRange = new Range();
2040 currentRange.length = fileLength;
2042 int dashPos = rangeDefinition.indexOf('-');
2044 if (dashPos == -1) {
2045 response.addHeader("Content-Range", "bytes */" + fileLength);
2046 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2052 long offset = Long.parseLong(rangeDefinition);
2053 currentRange.start = fileLength + offset;
2054 currentRange.end = fileLength - 1;
2055 } catch (NumberFormatException e) {
2056 response.addHeader("Content-Range", "bytes */" + fileLength);
2057 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2062 currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
2063 if (dashPos < rangeDefinition.length() - 1)
2064 currentRange.end = Long.parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length()));
2066 currentRange.end = fileLength - 1;
2067 } catch (NumberFormatException e) {
2068 response.addHeader("Content-Range", "bytes */" + fileLength);
2069 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2073 if (!currentRange.validate()) {
2074 response.addHeader("Content-Range", "bytes */" + fileLength);
2075 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2078 result.add(currentRange);
2084 * Check if the conditions specified in the optional If headers are
2087 * @param request The servlet request we are processing
2088 * @param response The servlet response we are creating
2089 * @param file the file resource against which the checks will be made
2090 * @param oldBody the old version of the file, if requested
2091 * @return boolean true if the resource meets all the specified conditions,
2092 * and false if any of the conditions is not satisfied, in which
2093 * case request processing is stopped
2094 * @throws IOException
2096 protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response,
2097 FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
2098 // TODO : Checking the WebDAV If header
2099 return checkIfMatch(request, response, file, oldBody) &&
2100 checkIfModifiedSince(request, response, file, oldBody) &&
2101 checkIfNoneMatch(request, response, file, oldBody) &&
2102 checkIfUnmodifiedSince(request, response, file, oldBody);
2106 * Check if the if-match condition is satisfied.
2108 * @param request The servlet request we are processing
2109 * @param response The servlet response we are creating
2110 * @param file the file object
2111 * @param oldBody the old version of the file, if requested
2112 * @return boolean true if the resource meets the specified condition, and
2113 * false if the condition is not satisfied, in which case request
2114 * processing is stopped
2115 * @throws IOException
2117 private boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response,
2118 FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
2119 String eTag = getETag(file, oldBody);
2120 String headerValue = request.getHeader("If-Match");
2121 if (headerValue != null)
2122 if (headerValue.indexOf('*') == -1) {
2123 StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
2124 boolean conditionSatisfied = false;
2125 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
2126 String currentToken = commaTokenizer.nextToken();
2127 if (currentToken.trim().equals(eTag))
2128 conditionSatisfied = true;
2130 // If none of the given ETags match, 412 Precodition failed is
2132 if (!conditionSatisfied) {
2133 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2141 * Check if the if-modified-since condition is satisfied.
2143 * @param request The servlet request we are processing
2144 * @param response The servlet response we are creating
2145 * @param file the file object
2146 * @param oldBody the old version of the file, if requested
2147 * @return boolean true if the resource meets the specified condition, and
2148 * false if the condition is not satisfied, in which case request
2149 * processing is stopped
2151 private boolean checkIfModifiedSince(HttpServletRequest request,
2152 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody) {
2154 long headerValue = request.getDateHeader("If-Modified-Since");
2155 long lastModified = oldBody == null ?
2156 file.getAuditInfo().getModificationDate().getTime() :
2157 oldBody.getAuditInfo().getModificationDate().getTime();
2158 if (headerValue != -1)
2159 // If an If-None-Match header has been specified, if modified
2160 // since is ignored.
2161 if (request.getHeader("If-None-Match") == null && lastModified < headerValue + 1000) {
2162 // The entity has not been modified since the date
2163 // specified by the client. This is not an error case.
2164 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
2165 response.setHeader("ETag", getETag(file, oldBody));
2168 } catch (IllegalArgumentException illegalArgument) {
2175 * Check if the if-none-match condition is satisfied.
2177 * @param request The servlet request we are processing
2178 * @param response The servlet response we are creating
2179 * @param file the file object
2180 * @param oldBody the old version of the file, if requested
2181 * @return boolean true if the resource meets the specified condition, and
2182 * false if the condition is not satisfied, in which case request
2183 * processing is stopped
2184 * @throws IOException
2186 private boolean checkIfNoneMatch(HttpServletRequest request,
2187 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody)
2188 throws IOException {
2189 String eTag = getETag(file, oldBody);
2190 String headerValue = request.getHeader("If-None-Match");
2191 if (headerValue != null) {
2192 boolean conditionSatisfied = false;
2193 if (!headerValue.equals("*")) {
2194 StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
2195 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
2196 String currentToken = commaTokenizer.nextToken();
2197 if (currentToken.trim().equals(eTag))
2198 conditionSatisfied = true;
2201 conditionSatisfied = true;
2202 if (conditionSatisfied) {
2203 // For GET and HEAD, we should respond with 304 Not Modified.
2204 // For every other method, 412 Precondition Failed is sent
2206 if ("GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod())) {
2207 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
2208 response.setHeader("ETag", getETag(file, oldBody));
2211 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2219 * Check if the if-unmodified-since condition is satisfied.
2221 * @param request The servlet request we are processing
2222 * @param response The servlet response we are creating
2223 * @param file the file object
2224 * @param oldBody the old version of the file, if requested
2225 * @return boolean true if the resource meets the specified condition, and
2226 * false if the condition is not satisfied, in which case request
2227 * processing is stopped
2228 * @throws IOException
2230 private boolean checkIfUnmodifiedSince(HttpServletRequest request,
2231 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody)
2232 throws IOException {
2234 long lastModified = oldBody == null ?
2235 file.getAuditInfo().getModificationDate().getTime() :
2236 oldBody.getAuditInfo().getModificationDate().getTime();
2237 long headerValue = request.getDateHeader("If-Unmodified-Since");
2238 if (headerValue != -1)
2239 if (lastModified >= headerValue + 1000) {
2240 // The entity has not been modified since the date
2241 // specified by the client. This is not an error case.
2242 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2245 } catch (IllegalArgumentException illegalArgument) {
2252 * Copy the contents of the specified input stream to the specified output
2253 * stream, and ensure that both streams are closed before returning (even in
2254 * the face of an exception).
2256 * @param file the file resource
2258 * @param ostream The output stream to write to
2259 * @param req the HTTP request
2260 * @param oldBody the old version of the file, if requested
2261 * @exception IOException if an input/output error occurs
2262 * @throws RpcException
2263 * @throws InsufficientPermissionsException
2264 * @throws ObjectNotFoundException
2266 protected void copy(FileHeaderDTO file, InputStream is, ServletOutputStream ostream,
2267 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2268 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2269 IOException exception = null;
2270 InputStream resourceInputStream = null;
2271 User user = getUser(req);
2272 // Files open for all will not have specified a calling user in the request.
2274 user = getOwner(req);
2276 throw new ObjectNotFoundException("No user or owner specified");
2278 resourceInputStream = oldBody == null ?
2279 getService().getFileContents(user.getId(), file.getId()) :
2280 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2282 resourceInputStream = is;
2284 InputStream istream = new BufferedInputStream(resourceInputStream, input);
2285 // Copy the input stream to the output stream
2286 exception = copyRange(istream, ostream);
2287 // Clean up the input stream
2289 // Rethrow any exception that has occurred
2290 if (exception != null)
2295 * Copy the contents of the specified input stream to the specified output
2296 * stream, and ensure that both streams are closed before returning (even in
2297 * the face of an exception).
2299 * @param istream The input stream to read from
2300 * @param ostream The output stream to write to
2301 * @return Exception which occurred during processing
2303 private IOException copyRange(InputStream istream, ServletOutputStream ostream) {
2304 // Copy the input stream to the output stream
2305 IOException exception = null;
2306 byte buffer[] = new byte[input];
2307 int len = buffer.length;
2310 len = istream.read(buffer);
2313 ostream.write(buffer, 0, len);
2314 } catch (IOException e) {
2323 * Copy the contents of the specified input stream to the specified output
2324 * stream, and ensure that both streams are closed before returning (even in
2325 * the face of an exception).
2329 * @param resourceInfo The resource info
2330 * @param writer The writer to write to
2331 * @param req the HTTP request
2332 * @param oldBody the old version of the file, if requested
2333 * @exception IOException if an input/output error occurs
2334 * @throws RpcException
2335 * @throws InsufficientPermissionsException
2336 * @throws ObjectNotFoundException
2338 protected void copy(FileHeaderDTO file, InputStream is, PrintWriter writer,
2339 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2340 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2341 IOException exception = null;
2343 User user = getUser(req);
2344 InputStream resourceInputStream = null;
2346 resourceInputStream = oldBody == null ?
2347 getService().getFileContents(user.getId(), file.getId()) :
2348 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2350 resourceInputStream = is;
2353 if (fileEncoding == null)
2354 reader = new InputStreamReader(resourceInputStream);
2356 reader = new InputStreamReader(resourceInputStream, fileEncoding);
2358 // Copy the input stream to the output stream
2359 exception = copyRange(reader, writer);
2360 // Clean up the reader
2362 // Rethrow any exception that has occurred
2363 if (exception != null)
2368 * Copy the contents of the specified input stream to the specified output
2369 * stream, and ensure that both streams are closed before returning (even in
2370 * the face of an exception).
2372 * @param reader The reader to read from
2373 * @param writer The writer to write to
2374 * @return Exception which occurred during processing
2376 private IOException copyRange(Reader reader, PrintWriter writer) {
2377 // Copy the input stream to the output stream
2378 IOException exception = null;
2379 char buffer[] = new char[input];
2380 int len = buffer.length;
2383 len = reader.read(buffer);
2386 writer.write(buffer, 0, len);
2387 } catch (IOException e) {
2396 * Copy the contents of the specified input stream to the specified output
2397 * stream, and ensure that both streams are closed before returning (even in
2398 * the face of an exception).
2401 * @param writer The writer to write to
2402 * @param ranges Enumeration of the ranges the client wanted to retrieve
2403 * @param contentType Content type of the resource
2404 * @param req the HTTP request
2405 * @param oldBody the old version of the file, if requested
2406 * @exception IOException if an input/output error occurs
2407 * @throws RpcException
2408 * @throws InsufficientPermissionsException
2409 * @throws ObjectNotFoundException
2411 protected void copy(FileHeaderDTO file, PrintWriter writer, Iterator ranges,
2412 String contentType, HttpServletRequest req, FileBodyDTO oldBody)
2413 throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2414 User user = getUser(req);
2415 IOException exception = null;
2416 while (exception == null && ranges.hasNext()) {
2417 InputStream resourceInputStream = oldBody == null ?
2418 getService().getFileContents(user.getId(), file.getId()) :
2419 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2421 if (fileEncoding == null)
2422 reader = new InputStreamReader(resourceInputStream);
2424 reader = new InputStreamReader(resourceInputStream, fileEncoding);
2425 Range currentRange = (Range) ranges.next();
2426 // Writing MIME header.
2428 writer.println("--" + mimeSeparation);
2429 if (contentType != null)
2430 writer.println("Content-Type: " + contentType);
2431 writer.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" + currentRange.length);
2434 exception = copyRange(reader, writer, currentRange.start, currentRange.end);
2438 writer.print("--" + mimeSeparation + "--");
2439 // Rethrow any exception that has occurred
2440 if (exception != null)
2445 * Copy the contents of the specified input stream to the specified output
2446 * stream, and ensure that both streams are closed before returning (even in
2447 * the face of an exception).
2449 * @param istream The input stream to read from
2450 * @param ostream The output stream to write to
2451 * @param start Start of the range which will be copied
2452 * @param end End of the range which will be copied
2453 * @return Exception which occurred during processing
2455 private IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) {
2456 if (logger.isDebugEnabled())
2457 logger.debug("Serving bytes:" + start + "-" + end);
2459 istream.skip(start);
2460 } catch (IOException e) {
2463 IOException exception = null;
2464 long bytesToRead = end - start + 1;
2465 byte buffer[] = new byte[input];
2466 int len = buffer.length;
2467 while (bytesToRead > 0 && len >= buffer.length) {
2469 len = istream.read(buffer);
2470 if (bytesToRead >= len) {
2471 ostream.write(buffer, 0, len);
2474 ostream.write(buffer, 0, (int) bytesToRead);
2477 } catch (IOException e) {
2481 if (len < buffer.length)
2488 * Copy the contents of the specified input stream to the specified output
2489 * stream, and ensure that both streams are closed before returning (even in
2490 * the face of an exception).
2492 * @param reader The reader to read from
2493 * @param writer The writer to write to
2494 * @param start Start of the range which will be copied
2495 * @param end End of the range which will be copied
2496 * @return Exception which occurred during processing
2498 private IOException copyRange(Reader reader, PrintWriter writer, long start, long end) {
2501 } catch (IOException e) {
2504 IOException exception = null;
2505 long bytesToRead = end - start + 1;
2506 char buffer[] = new char[input];
2507 int len = buffer.length;
2508 while (bytesToRead > 0 && len >= buffer.length) {
2510 len = reader.read(buffer);
2511 if (bytesToRead >= len) {
2512 writer.write(buffer, 0, len);
2515 writer.write(buffer, 0, (int) bytesToRead);
2518 } catch (IOException e) {
2522 if (len < buffer.length)
2529 * Copy the contents of the specified input stream to the specified output
2530 * stream, and ensure that both streams are closed before returning (even in
2531 * the face of an exception).
2534 * @param ostream The output stream to write to
2535 * @param range Range the client wanted to retrieve
2536 * @param req the HTTP request
2537 * @param oldBody the old version of the file, if requested
2538 * @exception IOException if an input/output error occurs
2539 * @throws RpcException
2540 * @throws InsufficientPermissionsException
2541 * @throws ObjectNotFoundException
2543 protected void copy(FileHeaderDTO file, ServletOutputStream ostream, Range range,
2544 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2545 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2546 IOException exception = null;
2547 User user = getUser(req);
2548 InputStream resourceInputStream = oldBody == null ?
2549 getService().getFileContents(user.getId(), file.getId()) :
2550 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2551 InputStream istream = new BufferedInputStream(resourceInputStream, input);
2552 exception = copyRange(istream, ostream, range.start, range.end);
2553 // Clean up the input stream
2555 // Rethrow any exception that has occurred
2556 if (exception != null)
2561 * Copy the contents of the specified input stream to the specified output
2562 * stream, and ensure that both streams are closed before returning (even in
2563 * the face of an exception).
2566 * @param writer The writer to write to
2567 * @param range Range the client wanted to retrieve
2568 * @param req the HTTP request
2569 * @param oldBody the old version of the file, if requested
2570 * @exception IOException if an input/output error occurs
2571 * @throws RpcException
2572 * @throws InsufficientPermissionsException
2573 * @throws ObjectNotFoundException
2575 protected void copy(FileHeaderDTO file, PrintWriter writer, Range range,
2576 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2577 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2578 IOException exception = null;
2579 User user = getUser(req);
2580 InputStream resourceInputStream = oldBody == null ?
2581 getService().getFileContents(user.getId(), file.getId()) :
2582 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2584 if (fileEncoding == null)
2585 reader = new InputStreamReader(resourceInputStream);
2587 reader = new InputStreamReader(resourceInputStream, fileEncoding);
2589 exception = copyRange(reader, writer, range.start, range.end);
2590 // Clean up the input stream
2592 // Rethrow any exception that has occurred
2593 if (exception != null)
2598 * Copy the contents of the specified input stream to the specified output
2599 * stream, and ensure that both streams are closed before returning (even in
2600 * the face of an exception).
2603 * @param ostream The output stream to write to
2604 * @param ranges Enumeration of the ranges the client wanted to retrieve
2605 * @param contentType Content type of the resource
2606 * @param req the HTTP request
2607 * @param oldBody the old version of the file, if requested
2608 * @exception IOException if an input/output error occurs
2609 * @throws RpcException
2610 * @throws InsufficientPermissionsException
2611 * @throws ObjectNotFoundException
2613 protected void copy(FileHeaderDTO file, ServletOutputStream ostream,
2614 Iterator ranges, String contentType, HttpServletRequest req,
2615 FileBodyDTO oldBody) throws IOException, ObjectNotFoundException,
2616 InsufficientPermissionsException, RpcException {
2617 IOException exception = null;
2618 User user = getUser(req);
2619 while (exception == null && ranges.hasNext()) {
2620 InputStream resourceInputStream = oldBody == null ?
2621 getService().getFileContents(user.getId(), file.getId()) :
2622 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2623 InputStream istream = new BufferedInputStream(resourceInputStream, input);
2624 Range currentRange = (Range) ranges.next();
2625 // Writing MIME header.
2627 ostream.println("--" + mimeSeparation);
2628 if (contentType != null)
2629 ostream.println("Content-Type: " + contentType);
2630 ostream.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" + currentRange.length);
2634 exception = copyRange(istream, ostream, currentRange.start, currentRange.end);
2639 ostream.print("--" + mimeSeparation + "--");
2640 // Rethrow any exception that has occurred
2641 if (exception != null)
2646 * Return an InputStream to an HTML representation of the contents of this
2649 * @param contextPath Context path to which our internal paths are relative
2650 * @param path the requested path to the resource
2651 * @param folder the specified directory
2652 * @param req the HTTP request
2653 * @return an input stream with the rendered contents
2654 * @throws IOException
2655 * @throws ServletException
2657 private InputStream renderHtml(String contextPath, String path, FolderDTO folder, HttpServletRequest req) throws IOException, ServletException {
2658 String name = folder.getName();
2659 // Prepare a writer to a buffered area
2660 ByteArrayOutputStream stream = new ByteArrayOutputStream();
2661 OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
2662 PrintWriter writer = new PrintWriter(osWriter);
2663 StringBuffer sb = new StringBuffer();
2664 // rewriteUrl(contextPath) is expensive. cache result for later reuse
2665 String rewrittenContextPath = rewriteUrl(contextPath);
2666 // Render the page header
2667 sb.append("<html>\r\n");
2668 sb.append("<head>\r\n");
2669 sb.append("<title>");
2670 sb.append("Index of " + name);
2671 sb.append("</title>\r\n");
2672 sb.append("<STYLE><!--");
2674 sb.append("--></STYLE> ");
2675 sb.append("</head>\r\n");
2676 sb.append("<body>");
2678 sb.append("Index of " + name);
2680 // Render the link to our parent (if required)
2681 String parentDirectory = path;
2682 if (parentDirectory.endsWith("/"))
2683 parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1);
2684 int slash = parentDirectory.lastIndexOf('/');
2686 String parent = path.substring(0, slash);
2687 sb.append(" - <a href=\"");
2688 sb.append(rewrittenContextPath);
2689 if (parent.equals(""))
2691 sb.append(rewriteUrl(parent));
2692 if (!parent.endsWith("/"))
2696 sb.append("Up To " + parent);
2702 sb.append("<HR size=\"1\" noshade=\"noshade\">");
2704 sb.append("<table width=\"100%\" cellspacing=\"0\"" + " cellpadding=\"5\" align=\"center\">\r\n");
2706 // Render the column headings
2707 sb.append("<tr>\r\n");
2708 sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
2710 sb.append("</strong></font></td>\r\n");
2711 sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
2713 sb.append("</strong></font></td>\r\n");
2714 sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
2715 sb.append("Last modified");
2716 sb.append("</strong></font></td>\r\n");
2718 // Render the directory entries within this directory
2719 boolean shade = false;
2720 Iterator iter = folder.getSubfolders().iterator();
2721 while (iter.hasNext()) {
2722 FolderDTO subf = (FolderDTO) iter.next();
2723 String resourceName = subf.getName();
2724 if (resourceName.equalsIgnoreCase("WEB-INF") || resourceName.equalsIgnoreCase("META-INF"))
2729 sb.append(" bgcolor=\"#eeeeee\"");
2733 sb.append("<td align=\"left\"> \r\n");
2734 sb.append("<a href=\"");
2735 sb.append(rewrittenContextPath);
2736 sb.append(rewriteUrl(path + resourceName));
2738 sb.append("\"><tt>");
2739 sb.append(RequestUtil.filter(resourceName));
2741 sb.append("</tt></a></td>\r\n");
2743 sb.append("<td align=\"right\"><tt>");
2744 sb.append(" ");
2745 sb.append("</tt></td>\r\n");
2747 sb.append("<td align=\"right\"><tt>");
2748 sb.append(getLastModifiedHttp(folder.getAuditInfo()));
2749 sb.append("</tt></td>\r\n");
2751 sb.append("</tr>\r\n");
2753 List<FileHeaderDTO> files;
2755 User user = getUser(req);
2756 files = getService().getFiles(user.getId(), folder.getId(), true);
2757 } catch (ObjectNotFoundException e) {
2758 throw new ServletException(e.getMessage());
2759 } catch (InsufficientPermissionsException e) {
2760 throw new ServletException(e.getMessage());
2761 } catch (RpcException e) {
2762 throw new ServletException(e.getMessage());
2764 for (FileHeaderDTO file : files) {
2765 String resourceName = file.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));
2779 sb.append("\"><tt>");
2780 sb.append(RequestUtil.filter(resourceName));
2781 sb.append("</tt></a></td>\r\n");
2783 sb.append("<td align=\"right\"><tt>");
2784 sb.append(renderSize(file.getFileSize()));
2785 sb.append("</tt></td>\r\n");
2787 sb.append("<td align=\"right\"><tt>");
2788 sb.append(getLastModifiedHttp(file.getAuditInfo()));
2789 sb.append("</tt></td>\r\n");
2791 sb.append("</tr>\r\n");
2794 // Render the page footer
2795 sb.append("</table>\r\n");
2797 sb.append("<HR size=\"1\" noshade=\"noshade\">");
2799 sb.append("<h3>").append(getServletContext().getServerInfo()).append("</h3>");
2800 sb.append("</body>\r\n");
2801 sb.append("</html>\r\n");
2803 // Return an input stream to the underlying bytes
2804 writer.write(sb.toString());
2806 return new ByteArrayInputStream(stream.toByteArray());
2811 * Render the specified file size (in bytes).
2813 * @param size File size (in bytes)
2814 * @return the size as a string
2816 private String renderSize(long size) {
2817 long leftSide = size / 1024;
2818 long rightSide = size % 1024 / 103; // Makes 1 digit
2819 if (leftSide == 0 && rightSide == 0 && size > 0)
2821 return "" + leftSide + "." + rightSide + " kb";
2827 * @param req Servlet request
2828 * @param resp Servlet response
2829 * @return boolean true if the copy is successful
2830 * @throws IOException
2832 private boolean copyResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
2833 // Parsing destination header
2834 String destinationPath = req.getHeader("Destination");
2835 if (destinationPath == null) {
2836 resp.sendError(WebdavStatus.SC_BAD_REQUEST);
2840 // Remove url encoding from destination
2841 destinationPath = RequestUtil.URLDecode(destinationPath, "UTF8");
2843 int protocolIndex = destinationPath.indexOf("://");
2844 if (protocolIndex >= 0) {
2845 // if the Destination URL contains the protocol, we can safely
2846 // trim everything upto the first "/" character after "://"
2847 int firstSeparator = destinationPath.indexOf("/", protocolIndex + 4);
2848 if (firstSeparator < 0)
2849 destinationPath = "/";
2851 destinationPath = destinationPath.substring(firstSeparator);
2853 String hostName = req.getServerName();
2854 if (hostName != null && destinationPath.startsWith(hostName))
2855 destinationPath = destinationPath.substring(hostName.length());
2857 int portIndex = destinationPath.indexOf(":");
2859 destinationPath = destinationPath.substring(portIndex);
2861 if (destinationPath.startsWith(":")) {
2862 int firstSeparator = destinationPath.indexOf("/");
2863 if (firstSeparator < 0)
2864 destinationPath = "/";
2866 destinationPath = destinationPath.substring(firstSeparator);
2870 // Normalize destination path (remove '.' and '..')
2871 destinationPath = RequestUtil.normalize(destinationPath);
2873 String contextPath = req.getContextPath();
2874 if (contextPath != null && destinationPath.startsWith(contextPath))
2875 destinationPath = destinationPath.substring(contextPath.length());
2877 String pathInfo = req.getPathInfo();
2878 if (pathInfo != null) {
2879 String servletPath = req.getServletPath();
2880 if (servletPath != null && destinationPath.startsWith(servletPath))
2881 destinationPath = destinationPath.substring(servletPath.length());
2884 if (logger.isDebugEnabled())
2885 logger.debug("Dest path :" + destinationPath);
2887 if (destinationPath.toUpperCase().startsWith("/WEB-INF") || destinationPath.toUpperCase().startsWith("/META-INF")) {
2888 resp.sendError(WebdavStatus.SC_FORBIDDEN);
2892 String path = getRelativePath(req);
2894 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
2895 resp.sendError(WebdavStatus.SC_FORBIDDEN);
2899 if (destinationPath.equals(path)) {
2900 resp.sendError(WebdavStatus.SC_FORBIDDEN);
2904 // Parsing overwrite header
2905 boolean overwrite = true;
2906 String overwriteHeader = req.getHeader("Overwrite");
2908 if (overwriteHeader != null)
2909 if (overwriteHeader.equalsIgnoreCase("T"))
2914 User user = getUser(req);
2915 // Overwriting the destination
2916 boolean exists = true;
2918 getService().getResourceAtPath(user.getId(), destinationPath, true);
2919 } catch (ObjectNotFoundException e) {
2921 } catch (RpcException e) {
2922 resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
2927 // Delete destination resource, if it exists
2929 if (!deleteResource(destinationPath, req, resp, true))
2932 resp.setStatus(WebdavStatus.SC_CREATED);
2933 } else // If the destination exists, then it's a conflict
2935 resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);
2938 resp.setStatus(WebdavStatus.SC_CREATED);
2940 // Copying source to destination.
2941 Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();
2944 result = copyResource(errorList, path, destinationPath, req);
2945 } catch (RpcException e) {
2946 resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
2949 if (!result || !errorList.isEmpty()) {
2950 sendReport(req, resp, errorList);
2957 * Copy a collection.
2959 * @param errorList Hashtable containing the list of errors which occurred
2960 * during the copy operation
2961 * @param source Path of the resource to be copied
2962 * @param theDest Destination path
2963 * @param req the HTTP request
2964 * @return boolean true if the copy is successful
2965 * @throws RpcException
2967 private boolean copyResource(Hashtable<String, Integer> errorList, String source, String theDest, HttpServletRequest req) throws RpcException {
2969 String dest = theDest;
2970 // Fix the destination path when copying collections.
2971 if (source.endsWith("/") && !dest.endsWith("/"))
2974 if (logger.isDebugEnabled())
2975 logger.debug("Copy: " + source + " To: " + dest);
2977 User user = getUser(req);
2978 Object object = null;
2980 object = getService().getResourceAtPath(user.getId(), source, true);
2981 } catch (ObjectNotFoundException e) {
2984 if (object instanceof FolderDTO) {
2985 FolderDTO folder = (FolderDTO) object;
2987 getService().copyFolder(user.getId(), folder.getId(), dest);
2988 } catch (ObjectNotFoundException e) {
2989 errorList.put(dest, new Integer(WebdavStatus.SC_CONFLICT));
2991 } catch (DuplicateNameException e) {
2992 errorList.put(dest, new Integer(WebdavStatus.SC_CONFLICT));
2994 } catch (InsufficientPermissionsException e) {
2995 errorList.put(dest, new Integer(WebdavStatus.SC_FORBIDDEN));
3000 String newSource = source;
3001 if (!source.endsWith("/"))
3003 String newDest = dest;
3004 if (!dest.endsWith("/"))
3006 // Recursively copy the subfolders.
3007 Iterator iter = folder.getSubfolders().iterator();
3008 while (iter.hasNext()) {
3009 FolderDTO subf = (FolderDTO) iter.next();
3010 String resourceName = subf.getName();
3011 copyResource(errorList, newSource + resourceName, newDest + resourceName, req);
3013 // Recursively copy the files.
3014 List<FileHeaderDTO> files;
3015 files = getService().getFiles(user.getId(), folder.getId(), true);
3016 for (FileHeaderDTO file : files) {
3017 String resourceName = file.getName();
3018 copyResource(errorList, newSource + resourceName, newDest + resourceName, req);
3020 } catch (RpcException e) {
3021 errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3023 } catch (ObjectNotFoundException e) {
3024 errorList.put(source, new Integer(WebdavStatus.SC_NOT_FOUND));
3026 } catch (InsufficientPermissionsException e) {
3027 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3031 } else if (object instanceof FileHeaderDTO) {
3032 FileHeaderDTO file = (FileHeaderDTO) object;
3034 getService().copyFile(user.getId(), file.getId(), dest);
3035 } catch (ObjectNotFoundException e) {
3036 errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3038 } catch (DuplicateNameException e) {
3039 errorList.put(source, new Integer(WebdavStatus.SC_CONFLICT));
3041 } catch (InsufficientPermissionsException e) {
3042 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3044 } catch (QuotaExceededException e) {
3045 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3047 } catch (GSSIOException e) {
3048 errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3052 errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3059 * Delete a resource.
3061 * @param req Servlet request
3062 * @param resp Servlet response
3063 * @return boolean true if the deletion is successful
3064 * @throws IOException
3066 private boolean deleteResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
3067 String path = getRelativePath(req);
3068 return deleteResource(path, req, resp, true);
3072 * Delete a resource.
3074 * @param path Path of the resource which is to be deleted
3075 * @param req Servlet request
3076 * @param resp Servlet response
3077 * @param setStatus Should the response status be set on successful
3079 * @return boolean true if the deletion is successful
3080 * @throws IOException
3082 private boolean deleteResource(String path, HttpServletRequest req, HttpServletResponse resp, boolean setStatus) throws IOException {
3083 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
3084 resp.sendError(WebdavStatus.SC_FORBIDDEN);
3087 String ifHeader = req.getHeader("If");
3088 if (ifHeader == null)
3091 String lockTokenHeader = req.getHeader("Lock-Token");
3092 if (lockTokenHeader == null)
3093 lockTokenHeader = "";
3095 if (isLocked(path, ifHeader + lockTokenHeader)) {
3096 resp.sendError(WebdavStatus.SC_LOCKED);
3100 User user = getUser(req);
3101 boolean exists = true;
3102 Object object = null;
3104 object = getService().getResourceAtPath(user.getId(), path, true);
3105 } catch (ObjectNotFoundException e) {
3107 } catch (RpcException e) {
3108 resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
3113 resp.sendError(WebdavStatus.SC_NOT_FOUND);
3117 FolderDTO folder = null;
3118 FileHeaderDTO file = null;
3119 if (object instanceof FolderDTO)
3120 folder = (FolderDTO) object;
3122 file = (FileHeaderDTO) object;
3126 getService().deleteFile(user.getId(), file.getId());
3127 } catch (InsufficientPermissionsException e) {
3128 resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
3130 } catch (ObjectNotFoundException e) {
3131 // Although we had already found the object, it was
3132 // probably deleted from another thread.
3133 resp.sendError(WebdavStatus.SC_NOT_FOUND);
3135 } catch (RpcException e) {
3136 resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
3139 else if (folder != null) {
3140 Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();
3141 deleteCollection(req, folder, path, errorList);
3143 getService().deleteFolder(user.getId(), folder.getId());
3144 } catch (InsufficientPermissionsException e) {
3145 errorList.put(path, new Integer(WebdavStatus.SC_METHOD_NOT_ALLOWED));
3146 } catch (ObjectNotFoundException e) {
3147 errorList.put(path, new Integer(WebdavStatus.SC_NOT_FOUND));
3148 } catch (RpcException e) {
3149 errorList.put(path, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3152 if (!errorList.isEmpty()) {
3153 sendReport(req, resp, errorList);
3158 resp.setStatus(WebdavStatus.SC_NO_CONTENT);
3163 * Deletes a collection.
3165 * @param req the HTTP request
3166 * @param folder the folder whose contents will be deleted
3167 * @param path Path to the collection to be deleted
3168 * @param errorList Contains the list of the errors which occurred
3170 private void deleteCollection(HttpServletRequest req, FolderDTO folder, String path, Hashtable<String, Integer> errorList) {
3172 if (logger.isDebugEnabled())
3173 logger.debug("Delete:" + path);
3175 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
3176 errorList.put(path, new Integer(WebdavStatus.SC_FORBIDDEN));
3180 String ifHeader = req.getHeader("If");
3181 if (ifHeader == null)
3184 String lockTokenHeader = req.getHeader("Lock-Token");
3185 if (lockTokenHeader == null)
3186 lockTokenHeader = "";
3188 Iterator iter = folder.getSubfolders().iterator();
3189 while (iter.hasNext()) {
3190 FolderDTO subf = (FolderDTO) iter.next();
3191 String childName = path;
3192 if (!childName.equals("/"))
3194 childName += subf.getName();
3196 if (isLocked(childName, ifHeader + lockTokenHeader))
3197 errorList.put(childName, new Integer(WebdavStatus.SC_LOCKED));
3200 User user = getUser(req);
3201 Object object = getService().getResourceAtPath(user.getId(), childName, true);
3202 FolderDTO childFolder = null;
3203 FileHeaderDTO childFile = null;
3204 if (object instanceof FolderDTO)
3205 childFolder = (FolderDTO) object;
3207 childFile = (FileHeaderDTO) object;
3208 if (childFolder != null) {
3209 deleteCollection(req, childFolder, childName, errorList);
3210 getService().deleteFolder(user.getId(), childFolder.getId());
3211 } else if (childFile != null)
3212 getService().deleteFile(user.getId(), childFile.getId());
3213 } catch (ObjectNotFoundException e) {
3214 errorList.put(childName, new Integer(WebdavStatus.SC_NOT_FOUND));
3215 } catch (InsufficientPermissionsException e) {
3216 errorList.put(childName, new Integer(WebdavStatus.SC_FORBIDDEN));
3217 } catch (RpcException e) {
3218 errorList.put(childName, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3224 * Send a multistatus element containing a complete error report to the
3227 * @param req Servlet request
3228 * @param resp Servlet response
3229 * @param errorList List of error to be displayed
3230 * @throws IOException
3232 private void sendReport(HttpServletRequest req, HttpServletResponse resp, Hashtable errorList) throws IOException {
3234 resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
3236 String absoluteUri = req.getRequestURI();
3237 String relativePath = getRelativePath(req);
3239 XMLWriter generatedXML = new XMLWriter();
3240 generatedXML.writeXMLHeader();
3242 generatedXML.writeElement(null, "multistatus" + generateNamespaceDeclarations(), XMLWriter.OPENING);
3244 Enumeration pathList = errorList.keys();
3245 while (pathList.hasMoreElements()) {
3247 String errorPath = (String) pathList.nextElement();
3248 int errorCode = ((Integer) errorList.get(errorPath)).intValue();
3250 generatedXML.writeElement(null, "response", XMLWriter.OPENING);
3252 generatedXML.writeElement(null, "href", XMLWriter.OPENING);
3253 String toAppend = errorPath.substring(relativePath.length());
3254 if (!toAppend.startsWith("/"))
3255 toAppend = "/" + toAppend;
3256 generatedXML.writeText(absoluteUri + toAppend);
3257 generatedXML.writeElement(null, "href", XMLWriter.CLOSING);
3258 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
3259 generatedXML.writeText("HTTP/1.1 " + errorCode + " " + WebdavStatus.getStatusText(errorCode));
3260 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
3262 generatedXML.writeElement(null, "response", XMLWriter.CLOSING);
3266 generatedXML.writeElement(null, "multistatus", XMLWriter.CLOSING);
3268 Writer writer = resp.getWriter();
3269 writer.write(generatedXML.toString());
3274 // --------------------------------------------- WebdavResolver Inner Class
3276 * Work around for XML parsers that don't fully respect
3277 * {@link DocumentBuilderFactory#setExpandEntityReferences(boolean)}.
3278 * External references are filtered out for security reasons. See
3281 private class WebdavResolver implements EntityResolver {
3284 * A private copy of the servlet context.
3286 private ServletContext context;
3289 * Construct the resolver by passing the servlet context.
3291 * @param theContext the servlet context
3293 public WebdavResolver(ServletContext theContext) {
3294 context = theContext;
3297 public InputSource resolveEntity(String publicId, String systemId) {
3298 context.log("The request included a reference to an external entity with PublicID " + publicId + " and SystemID " + systemId + " which was ignored");
3299 return new InputSource(new StringReader("Ignored external entity"));
3304 * Returns the user making the request. This is the user whose credentials
3305 * were supplied in the authorization header.
3307 * @param req the HTTP request
3308 * @return the user making the request
3310 protected User getUser(HttpServletRequest req) {
3311 return (User) req.getAttribute(USER_ATTRIBUTE);
3315 * Retrieves the user who owns the requested namespace, as specified in the
3318 * @param req the HTTP request
3319 * @return the owner of the namespace
3321 protected User getOwner(HttpServletRequest req) {
3322 return (User) req.getAttribute(OWNER_ATTRIBUTE);