Throw any exceptions thrown unwrapped. This way, the caller knows what it's dealing...
[pithos] / src / gr / ebs / gss / server / webdav / Webdav.java
1 /*
2  * Copyright 2007, 2008, 2009 Electronic Business Systems Ltd.
3  *
4  * This file is part of GSS.
5  *
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.
10  *
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.
15  *
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/>.
18  */
19 package gr.ebs.gss.server.webdav;
20
21 import static gr.ebs.gss.server.configuration.GSSConfigurationFactory.getConfiguration;
22 import gr.ebs.gss.client.exceptions.DuplicateNameException;
23 import gr.ebs.gss.client.exceptions.GSSIOException;
24 import gr.ebs.gss.client.exceptions.InsufficientPermissionsException;
25 import gr.ebs.gss.client.exceptions.ObjectNotFoundException;
26 import gr.ebs.gss.client.exceptions.QuotaExceededException;
27 import gr.ebs.gss.client.exceptions.RpcException;
28 import gr.ebs.gss.server.domain.User;
29 import gr.ebs.gss.server.domain.dto.AuditInfoDTO;
30 import gr.ebs.gss.server.domain.dto.FileBodyDTO;
31 import gr.ebs.gss.server.domain.dto.FileHeaderDTO;
32 import gr.ebs.gss.server.domain.dto.FolderDTO;
33 import gr.ebs.gss.server.ejb.ExternalAPI;
34 import gr.ebs.gss.server.ejb.TransactionHelper;
35
36 import java.io.BufferedInputStream;
37 import java.io.ByteArrayInputStream;
38 import java.io.ByteArrayOutputStream;
39 import java.io.File;
40 import java.io.FileInputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.InputStreamReader;
44 import java.io.OutputStreamWriter;
45 import java.io.PrintWriter;
46 import java.io.RandomAccessFile;
47 import java.io.Reader;
48 import java.io.StringReader;
49 import java.io.StringWriter;
50 import java.io.UnsupportedEncodingException;
51 import java.io.Writer;
52 import java.net.URLDecoder;
53 import java.security.MessageDigest;
54 import java.security.NoSuchAlgorithmException;
55 import java.text.SimpleDateFormat;
56 import java.util.ArrayList;
57 import java.util.Date;
58 import java.util.Enumeration;
59 import java.util.Hashtable;
60 import java.util.Iterator;
61 import java.util.List;
62 import java.util.Locale;
63 import java.util.Stack;
64 import java.util.StringTokenizer;
65 import java.util.TimeZone;
66 import java.util.Vector;
67 import java.util.concurrent.Callable;
68
69 import javax.naming.Context;
70 import javax.naming.InitialContext;
71 import javax.naming.NamingException;
72 import javax.rmi.PortableRemoteObject;
73 import javax.servlet.ServletContext;
74 import javax.servlet.ServletException;
75 import javax.servlet.ServletOutputStream;
76 import javax.servlet.UnavailableException;
77 import javax.servlet.http.HttpServlet;
78 import javax.servlet.http.HttpServletRequest;
79 import javax.servlet.http.HttpServletResponse;
80 import javax.xml.parsers.DocumentBuilder;
81 import javax.xml.parsers.DocumentBuilderFactory;
82 import javax.xml.parsers.ParserConfigurationException;
83
84 import org.apache.commons.httpclient.HttpStatus;
85 import org.apache.commons.logging.Log;
86 import org.apache.commons.logging.LogFactory;
87 import org.w3c.dom.Document;
88 import org.w3c.dom.Element;
89 import org.w3c.dom.Node;
90 import org.w3c.dom.NodeList;
91 import org.xml.sax.EntityResolver;
92 import org.xml.sax.InputSource;
93 import org.xml.sax.SAXException;
94
95 /**
96  * The implementation of the WebDAV service.
97  *
98  * @author past
99  */
100 public class Webdav extends HttpServlet {
101
102         /**
103          * The request attribute containing the user who owns the requested
104          * namespace.
105          */
106         protected static final String OWNER_ATTRIBUTE = "owner";
107
108         /**
109          * The request attribute containing the user making the request.
110          */
111         protected static final String USER_ATTRIBUTE = "user";
112
113         /**
114          * The logger.
115          */
116         private static Log logger = LogFactory.getLog(Webdav.class);
117
118         /**
119      *
120      */
121         protected static final String METHOD_GET = "GET";
122
123         /**
124      *
125      */
126         protected static final String METHOD_POST = "POST";
127
128         /**
129      *
130      */
131         protected static final String METHOD_PUT = "PUT";
132
133         /**
134      *
135      */
136         protected static final String METHOD_DELETE = "DELETE";
137
138         /**
139      *
140      */
141         protected static final String METHOD_HEAD = "HEAD";
142
143         /**
144      *
145      */
146         private static final String METHOD_OPTIONS = "OPTIONS";
147
148         /**
149      *
150      */
151         private static final String METHOD_PROPFIND = "PROPFIND";
152
153         /**
154      *
155      */
156         private static final String METHOD_PROPPATCH = "PROPPATCH";
157
158         /**
159      *
160      */
161         private static final String METHOD_MKCOL = "MKCOL";
162
163         /**
164      *
165      */
166         private static final String METHOD_COPY = "COPY";
167
168         /**
169      *
170      */
171         private static final String METHOD_MOVE = "MOVE";
172
173         /**
174      *
175      */
176         private static final String METHOD_LOCK = "LOCK";
177
178         /**
179      *
180      */
181         private static final String METHOD_UNLOCK = "UNLOCK";
182
183         /**
184          * Default depth is infinite.
185          */
186         static final int INFINITY = 3; // To limit tree browsing a bit
187
188         /**
189          * PROPFIND - Specify a property mask.
190          */
191         private static final int FIND_BY_PROPERTY = 0;
192
193         /**
194          * PROPFIND - Display all properties.
195          */
196         private static final int FIND_ALL_PROP = 1;
197
198         /**
199          * PROPFIND - Return property names.
200          */
201         private static final int FIND_PROPERTY_NAMES = 2;
202
203         /**
204          * Default namespace.
205          */
206         private static final String DEFAULT_NAMESPACE = "DAV:";
207
208         /**
209          * Create a new lock.
210          */
211         private static final int LOCK_CREATION = 0;
212
213         /**
214          * Refresh lock.
215          */
216         private static final int LOCK_REFRESH = 1;
217
218         /**
219          * Default lock timeout value.
220          */
221         private static final int DEFAULT_TIMEOUT = 3600;
222
223         /**
224          * Maximum lock timeout.
225          */
226         private static final int MAX_TIMEOUT = 604800;
227
228         /**
229          * Size of file transfer buffer in bytes.
230          */
231         private static final int BUFFER_SIZE = 4096;
232
233         /**
234          * The output buffer size to use when serving resources.
235          */
236         protected int output = 2048;
237
238         /**
239          * The input buffer size to use when serving resources.
240          */
241         private int input = 2048;
242
243         /**
244          * MIME multipart separation string
245          */
246         protected static final String mimeSeparation = "GSS_MIME_BOUNDARY";
247
248         /**
249          * Simple date format for the creation date ISO representation (partial).
250          */
251         private static final SimpleDateFormat creationDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
252
253         /**
254          * HTTP date format.
255          */
256         private static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
257
258         /**
259          * Array containing the safe characters set.
260          */
261         private static URLEncoder urlEncoder;
262
263         /**
264          * File encoding to be used when reading static files. If none is specified
265          * the platform default is used.
266          */
267         private String fileEncoding = null;
268
269         /**
270          * The style sheet for displaying the directory listings.
271          */
272         private static final String GSS_CSS = "H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} " + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} " + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} " + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} " + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} " + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}" + "A {color : black;}" + "A.name {color : black;}" + "HR {color : #525D76;}";
273
274         /**
275          * Secret information used to generate reasonably secure lock ids.
276          */
277         private String secret = "gss-webdav";
278
279
280         /**
281          * Full range marker.
282          */
283         protected static ArrayList FULL = new ArrayList();
284
285         /**
286          * MD5 message digest provider.
287          */
288         protected static MessageDigest md5Helper;
289
290         /**
291          * The MD5 helper object for this class.
292          */
293         protected static final MD5Encoder md5Encoder = new MD5Encoder();
294
295         /**
296          * GMT timezone - all HTTP dates are on GMT
297          */
298         static {
299                 creationDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
300                 urlEncoder = new URLEncoder();
301                 urlEncoder.addSafeCharacter('-');
302                 urlEncoder.addSafeCharacter('_');
303                 urlEncoder.addSafeCharacter('.');
304                 urlEncoder.addSafeCharacter('*');
305                 urlEncoder.addSafeCharacter('/');
306         }
307
308         @Override
309         public void init() throws ServletException {
310                 if (getServletConfig().getInitParameter("input") != null)
311                         input = Integer.parseInt(getServletConfig().getInitParameter("input"));
312
313                 if (getServletConfig().getInitParameter("output") != null)
314                         output = Integer.parseInt(getServletConfig().getInitParameter("output"));
315
316                 fileEncoding = getServletConfig().getInitParameter("fileEncoding");
317
318                 // Sanity check on the specified buffer sizes
319                 if (input < 256)
320                         input = 256;
321                 if (output < 256)
322                         output = 256;
323                 if (logger.isDebugEnabled())
324                         logger.debug("Input buffer size=" + input + ", output buffer size=" + output);
325
326                 if (getServletConfig().getInitParameter("secret") != null)
327                         secret = getServletConfig().getInitParameter("secret");
328
329                 // Load the MD5 helper used to calculate signatures.
330                 try {
331                         md5Helper = MessageDigest.getInstance("MD5");
332                 } catch (NoSuchAlgorithmException e) {
333                         throw new UnavailableException("No MD5");
334                 }
335         }
336
337         /**
338          * A helper method that retrieves a reference to the ExternalAPI bean and
339          * stores it for future use.
340          *
341          * @return an ExternalAPI instance
342          * @throws RpcException in case an error occurs
343          */
344         protected ExternalAPI getService() throws RpcException {
345                 try {
346                         final Context ctx = new InitialContext();
347                         final Object ref = ctx.lookup(getConfiguration().getString("externalApiPath"));
348                         return (ExternalAPI) PortableRemoteObject.narrow(ref, ExternalAPI.class);
349                 } catch (final NamingException e) {
350                         logger.error("Unable to retrieve the ExternalAPI EJB", e);
351                         throw new RpcException("An error occurred while contacting the naming service");
352                 }
353         }
354
355         private void updateAccounting(final User user, final Date date, final long bandwidthDiff) {
356                 try {
357                         new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
358                                 @Override
359                                 public Void call() throws Exception {
360                                         getService().updateAccounting(user, date, bandwidthDiff);
361                                         return null;
362                                 }
363                         });
364                 } catch (RuntimeException e) {
365                         throw e;
366                 } catch (Exception e) {
367                         // updateAccounting() doesn't throw any checked exceptions
368                         assert false;
369                 }
370         }
371
372         @Override
373         public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException {
374                 String method = request.getMethod();
375
376                 if (logger.isDebugEnabled()) {
377                         String path = request.getPathInfo();
378                         if (path == null)
379                                 path = request.getServletPath();
380                         if (path == null || path.equals(""))
381                                 path = "/";
382                         logger.debug("[" + method + "] " + path);
383                 }
384
385                 try {
386                         User user = null;
387                         if (request.getUserPrincipal() != null) { // Let unauthenticated
388                                                                                                                 // OPTIONS go through;
389                                                                                                                 // all others will be
390                                                                                                                 // blocked by
391                                                                                                                 // authentication anyway
392                                                                                                                 // before we get here.
393                                 user = getService().findUser(request.getUserPrincipal().getName());
394                                 if (user == null) {
395                                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
396                                         return;
397                                 }
398                         }
399                         request.setAttribute(USER_ATTRIBUTE, user);
400                         request.setAttribute(OWNER_ATTRIBUTE, user);
401                 } catch (RpcException e) {
402                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
403                         return;
404                 }
405                 if (method.equals(METHOD_GET))
406                         doGet(request, response);
407                 else if (method.equals(METHOD_POST))
408                         doPost(request, response);
409                 else if (method.equals(METHOD_PUT))
410                         doPut(request, response);
411                 else if (method.equals(METHOD_DELETE))
412                         doDelete(request, response);
413                 else if (method.equals(METHOD_HEAD))
414                         doHead(request, response);
415                 else if (method.equals(METHOD_PROPFIND))
416                         doPropfind(request, response);
417                 else if (method.equals(METHOD_PROPPATCH))
418                         doProppatch(request, response);
419                 else if (method.equals(METHOD_MKCOL))
420                         doMkcol(request, response);
421                 else if (method.equals(METHOD_COPY))
422                         doCopy(request, response);
423                 else if (method.equals(METHOD_MOVE))
424                         doMove(request, response);
425                 else if (method.equals(METHOD_LOCK))
426                         doLock(request, response);
427                 else if (method.equals(METHOD_UNLOCK))
428                         doUnlock(request, response);
429                 else if (method.equals(METHOD_OPTIONS))
430                         doOptions(request, response);
431                 else
432                         // DefaultServlet processing for TRACE, etc.
433                         super.service(request, response);
434         }
435
436         @Override
437         protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws IOException {
438                 resp.addHeader("DAV", "1,2");
439                 StringBuffer methodsAllowed = new StringBuffer();
440                 try {
441                         methodsAllowed = determineMethodsAllowed(req);
442                 } catch (RpcException e) {
443                         resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
444                         return;
445                 }
446                 resp.addHeader("Allow", methodsAllowed.toString());
447                 resp.addHeader("MS-Author-Via", "DAV");
448         }
449
450         /**
451          * Implement the PROPFIND method.
452          *
453          * @param req the HTTP request
454          * @param resp the HTTP response
455          * @throws ServletException
456          * @throws IOException
457          */
458         private void doPropfind(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
459                 String path = getRelativePath(req);
460                 if (path.endsWith("/") && !path.equals("/"))
461                         path = path.substring(0, path.length() - 1);
462
463                 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
464                         resp.sendError(WebdavStatus.SC_FORBIDDEN);
465                         return;
466                 }
467
468                 // Properties which are to be displayed.
469                 Vector<String> properties = null;
470                 // Propfind depth
471                 int depth = INFINITY;
472                 // Propfind type
473                 int type = FIND_ALL_PROP;
474
475                 String depthStr = req.getHeader("Depth");
476
477                 if (depthStr == null)
478                         depth = INFINITY;
479                 else if (depthStr.equals("0"))
480                         depth = 0;
481                 else if (depthStr.equals("1"))
482                         depth = 1;
483                 else if (depthStr.equals("infinity"))
484                         depth = INFINITY;
485
486                 Node propNode = null;
487
488                 if (req.getInputStream().available() > 0) {
489                         DocumentBuilder documentBuilder = getDocumentBuilder();
490
491                         try {
492                                 Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
493
494                                 // Get the root element of the document
495                                 Element rootElement = document.getDocumentElement();
496                                 NodeList childList = rootElement.getChildNodes();
497
498                                 for (int i = 0; i < childList.getLength(); i++) {
499                                         Node currentNode = childList.item(i);
500                                         switch (currentNode.getNodeType()) {
501                                                 case Node.TEXT_NODE:
502                                                         break;
503                                                 case Node.ELEMENT_NODE:
504                                                         if (currentNode.getNodeName().endsWith("prop")) {
505                                                                 type = FIND_BY_PROPERTY;
506                                                                 propNode = currentNode;
507                                                         }
508                                                         if (currentNode.getNodeName().endsWith("propname"))
509                                                                 type = FIND_PROPERTY_NAMES;
510                                                         if (currentNode.getNodeName().endsWith("allprop"))
511                                                                 type = FIND_ALL_PROP;
512                                                         break;
513                                         }
514                                 }
515                         } catch (SAXException e) {
516                                 // Something went wrong - use the defaults.
517                                 if (logger.isDebugEnabled())
518                                         logger.debug(e.getMessage());
519                         } catch (IOException e) {
520                                 // Something went wrong - use the defaults.
521                                 if (logger.isDebugEnabled())
522                                         logger.debug(e.getMessage());
523                         }
524                 }
525
526                 if (type == FIND_BY_PROPERTY) {
527                         properties = new Vector<String>();
528                         NodeList childList = propNode.getChildNodes();
529
530                         for (int i = 0; i < childList.getLength(); i++) {
531                                 Node currentNode = childList.item(i);
532                                 switch (currentNode.getNodeType()) {
533                                         case Node.TEXT_NODE:
534                                                 break;
535                                         case Node.ELEMENT_NODE:
536                                                 String nodeName = currentNode.getNodeName();
537                                                 String propertyName = null;
538                                                 if (nodeName.indexOf(':') != -1)
539                                                         propertyName = nodeName.substring(nodeName.indexOf(':') + 1);
540                                                 else
541                                                         propertyName = nodeName;
542                                                 // href is a live property which is handled differently
543                                                 properties.addElement(propertyName);
544                                                 break;
545                                 }
546                         }
547                 }
548                 User user = getUser(req);
549                 boolean exists = true;
550                 Object object = null;
551                 try {
552                         object = getService().getResourceAtPath(user.getId(), path, true);
553                 } catch (ObjectNotFoundException e) {
554                         exists = false;
555                 } catch (RpcException e) {
556                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
557                         return;
558                 }
559                 if (!exists) {
560                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
561                         return;
562                 }
563                 resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
564                 resp.setContentType("text/xml; charset=UTF-8");
565                 // Create multistatus object
566                 XMLWriter generatedXML = new XMLWriter(resp.getWriter());
567                 generatedXML.writeXMLHeader();
568                 generatedXML.writeElement(null, "multistatus" + generateNamespaceDeclarations(), XMLWriter.OPENING);
569                 if (depth == 0)
570                         parseProperties(req, generatedXML, path, type, properties, object);
571                 else {
572                         // The stack always contains the object of the current level
573                         Stack<String> stack = new Stack<String>();
574                         stack.push(path);
575
576                         // Stack of the objects one level below
577                         Stack<String> stackBelow = new Stack<String>();
578                         while (!stack.isEmpty() && depth >= 0) {
579                                 String currentPath = stack.pop();
580                                 try {
581                                         object = getService().getResourceAtPath(user.getId(), currentPath, true);
582                                 } catch (ObjectNotFoundException e) {
583                                         continue;
584                                 } catch (RpcException e) {
585                                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
586                                         return;
587                                 }
588                                 parseProperties(req, generatedXML, currentPath, type, properties, object);
589                                 if (object instanceof FolderDTO && depth > 0) {
590                                         FolderDTO folder = (FolderDTO) object;
591                                         // Retrieve the subfolders.
592                                         List subfolders = folder.getSubfolders();
593                                         Iterator iter = subfolders.iterator();
594                                         while (iter.hasNext()) {
595                                                 FolderDTO f = (FolderDTO) iter.next();
596                                                 String newPath = currentPath;
597                                                 if (!newPath.endsWith("/"))
598                                                         newPath += "/";
599                                                 newPath += f.getName();
600                                                 stackBelow.push(newPath);
601                                         }
602                                         // Retrieve the files.
603                                         List<FileHeaderDTO> files;
604                                         try {
605                                                 files = getService().getFiles(user.getId(), folder.getId(), true);
606                                         } catch (ObjectNotFoundException e) {
607                                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
608                                                 return;
609                                         } catch (InsufficientPermissionsException e) {
610                                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN, path);
611                                                 return;
612                                         } catch (RpcException e) {
613                                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
614                                                 return;
615                                         }
616                                         for (FileHeaderDTO file : files) {
617                                                 String newPath = currentPath;
618                                                 if (!newPath.endsWith("/"))
619                                                         newPath += "/";
620                                                 newPath += file.getName();
621                                                 stackBelow.push(newPath);
622                                         }
623                                 }
624                                 if (stack.isEmpty()) {
625                                         depth--;
626                                         stack = stackBelow;
627                                         stackBelow = new Stack<String>();
628                                 }
629                                 generatedXML.sendData();
630                         }
631                 }
632                 generatedXML.writeElement(null, "multistatus", XMLWriter.CLOSING);
633                 generatedXML.sendData();
634         }
635
636         /**
637          * PROPPATCH Method.
638          *
639          * @param req the HTTP request
640          * @param resp the HTTP response
641          * @throws IOException if an error occurs while sending the response
642          */
643         private void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
644                 if (isLocked(req)) {
645                         resp.sendError(WebdavStatus.SC_LOCKED);
646                         return;
647                 }
648                 resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
649         }
650
651         /* (non-Javadoc)
652          * @see javax.servlet.http.HttpServlet#doDelete(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
653          */
654         @Override
655         protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
656                 if (isLocked(req)) {
657                         resp.sendError(WebdavStatus.SC_LOCKED);
658                         return;
659                 }
660                 deleteResource(req, resp);
661         }
662
663         @Override
664         protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
665                 // Serve the requested resource, including the data content
666                 try {
667                         serveResource(req, resp, true);
668                 } catch (ObjectNotFoundException e) {
669                         resp.sendError(HttpServletResponse.SC_NOT_FOUND);
670                         return;
671                 } catch (InsufficientPermissionsException e) {
672                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
673                         return;
674                 } catch (RpcException e) {
675                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
676                         return;
677                 }
678         }
679
680         @Override
681         protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
682                 resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
683         }
684
685         @Override
686         protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
687                 if (isLocked(req)) {
688                         resp.sendError(WebdavStatus.SC_LOCKED);
689                         return;
690                 }
691
692                 final User user = getUser(req);
693                 String path = getRelativePath(req);
694                 boolean exists = true;
695                 Object resource = null;
696                 FileHeaderDTO file = null;
697                 try {
698                         resource = getService().getResourceAtPath(user.getId(), path, true);
699                 } catch (ObjectNotFoundException e) {
700                         exists = false;
701                 } catch (RpcException e) {
702                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
703                         return;
704                 }
705
706                 if (exists)
707                         if (resource instanceof FileHeaderDTO)
708                                 file = (FileHeaderDTO) resource;
709                         else {
710                                 resp.sendError(HttpServletResponse.SC_CONFLICT);
711                                 return;
712                         }
713                 boolean result = true;
714
715                 // Temporary content file used to support partial PUT.
716                 File contentFile = null;
717
718                 Range range = parseContentRange(req, resp);
719
720                 InputStream resourceInputStream = null;
721
722                 // Append data specified in ranges to existing content for this
723                 // resource - create a temporary file on the local filesystem to
724                 // perform this operation.
725                 // Assume just one range is specified for now
726                 if (range != null) {
727                         try {
728                                 contentFile = executePartialPut(req, range, path);
729                         } catch (RpcException e) {
730                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
731                                 return;
732                         } catch (ObjectNotFoundException e) {
733                                 resp.sendError(HttpServletResponse.SC_CONFLICT);
734                                 return;
735                         } catch (InsufficientPermissionsException e) {
736                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
737                                 return;
738                         }
739                         resourceInputStream = new FileInputStream(contentFile);
740                 } else
741                         resourceInputStream = req.getInputStream();
742
743                 try {
744                         Object parent = getService().getResourceAtPath(user.getId(), getParentPath(path), true);
745                         if (!(parent instanceof FolderDTO)) {
746                                 resp.sendError(HttpServletResponse.SC_CONFLICT);
747                                 return;
748                         }
749                         final FolderDTO folder = (FolderDTO) parent;
750                         final String name = getLastElement(path);
751                         final String mimeType = getServletContext().getMimeType(name);
752                 File uploadedFile = null;
753                 try {
754                                 uploadedFile = getService().uploadFile(resourceInputStream, user.getId());
755                         } catch (IOException ex) {
756                                 throw new GSSIOException(ex, false);
757                         }
758                         // FIXME: Add attributes
759                         FileHeaderDTO fileDTO = null;
760                         final FileHeaderDTO f = file;
761                         final File uf = uploadedFile;
762                         if (exists)
763                                 fileDTO = new TransactionHelper<FileHeaderDTO>().tryExecute(new Callable<FileHeaderDTO>() {
764                                         @Override
765                                         public FileHeaderDTO call() throws Exception {
766                                                 return getService().updateFileContents(user.getId(), f.getId(), mimeType, uf.length(), uf.getAbsolutePath());
767                                         }
768                                 });
769                         else
770                                 fileDTO = new TransactionHelper<FileHeaderDTO>().tryExecute(new Callable<FileHeaderDTO>() {
771                                         @Override
772                                         public FileHeaderDTO call() throws Exception {
773                                                 return getService().createFile(user.getId(), folder.getId(), name, mimeType, uf.length(), uf.getAbsolutePath());
774                                         }
775                                 });
776                         updateAccounting(user, new Date(), fileDTO.getFileSize());
777                 } catch (ObjectNotFoundException e) {
778                         result = false;
779                 } catch (InsufficientPermissionsException e) {
780                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
781                         return;
782                 } catch (QuotaExceededException e) {
783                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
784                         return;
785                 } catch (GSSIOException e) {
786                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
787                         return;
788                 } catch (RpcException e) {
789                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
790                         return;
791                 } catch (DuplicateNameException e) {
792                         resp.sendError(HttpServletResponse.SC_CONFLICT);
793                         return;
794                 } catch (Exception e) {
795                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
796                         return;
797                 }
798
799                 if (result) {
800                         if (exists)
801                                 resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
802                         else
803                                 resp.setStatus(HttpServletResponse.SC_CREATED);
804                 } else
805                         resp.sendError(HttpServletResponse.SC_CONFLICT);
806
807         }
808
809         @Override
810         protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
811                 // Serve the requested resource, without the data content
812                 try {
813                         serveResource(req, resp, false);
814                 } catch (ObjectNotFoundException e) {
815                         resp.sendError(HttpServletResponse.SC_NOT_FOUND);
816                         return;
817                 } catch (InsufficientPermissionsException e) {
818                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
819                         return;
820                 } catch (RpcException e) {
821                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
822                         return;
823                 }
824         }
825
826         /**
827          * The UNLOCK method.
828          *
829          * @param req the HTTP request
830          * @param resp the HTTP response
831          * @throws IOException if an error occurs while sending the response
832          */
833         private void doUnlock(@SuppressWarnings("unused") HttpServletRequest req, HttpServletResponse resp) throws IOException {
834                 resp.setStatus(WebdavStatus.SC_NO_CONTENT);
835         }
836
837         /**
838          * The LOCK method.
839          *
840          * @param req the HTTP request
841          * @param resp the HTTP response
842          * @throws IOException if an error occurs while sending the response
843          * @throws ServletException
844          */
845         private void doLock(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
846                 LockInfo lock = new LockInfo();
847                 // Parsing lock request
848
849                 // Parsing depth header
850                 String depthStr = req.getHeader("Depth");
851                 if (depthStr == null)
852                         lock.depth = INFINITY;
853                 else if (depthStr.equals("0"))
854                         lock.depth = 0;
855                 else
856                         lock.depth = INFINITY;
857
858                 // Parsing timeout header
859                 int lockDuration = DEFAULT_TIMEOUT;
860                 String lockDurationStr = req.getHeader("Timeout");
861                 if (lockDurationStr == null)
862                         lockDuration = DEFAULT_TIMEOUT;
863                 else {
864                         int commaPos = lockDurationStr.indexOf(",");
865                         // If multiple timeouts, just use the first
866                         if (commaPos != -1)
867                                 lockDurationStr = lockDurationStr.substring(0, commaPos);
868                         if (lockDurationStr.startsWith("Second-"))
869                                 lockDuration = new Integer(lockDurationStr.substring(7)).intValue();
870                         else if (lockDurationStr.equalsIgnoreCase("infinity"))
871                                 lockDuration = MAX_TIMEOUT;
872                         else
873                                 try {
874                                         lockDuration = new Integer(lockDurationStr).intValue();
875                                 } catch (NumberFormatException e) {
876                                         lockDuration = MAX_TIMEOUT;
877                                 }
878                         if (lockDuration == 0)
879                                 lockDuration = DEFAULT_TIMEOUT;
880                         if (lockDuration > MAX_TIMEOUT)
881                                 lockDuration = MAX_TIMEOUT;
882                 }
883                 lock.expiresAt = System.currentTimeMillis() + lockDuration * 1000;
884
885                 int lockRequestType = LOCK_CREATION;
886                 Node lockInfoNode = null;
887                 DocumentBuilder documentBuilder = getDocumentBuilder();
888
889                 try {
890                         Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
891                         // Get the root element of the document
892                         Element rootElement = document.getDocumentElement();
893                         lockInfoNode = rootElement;
894                 } catch (IOException e) {
895                         lockRequestType = LOCK_REFRESH;
896                 } catch (SAXException e) {
897                         lockRequestType = LOCK_REFRESH;
898                 }
899
900                 if (lockInfoNode != null) {
901                         // Reading lock information
902                         NodeList childList = lockInfoNode.getChildNodes();
903                         StringWriter strWriter = null;
904                         DOMWriter domWriter = null;
905
906                         Node lockScopeNode = null;
907                         Node lockTypeNode = null;
908                         Node lockOwnerNode = null;
909
910                         for (int i = 0; i < childList.getLength(); i++) {
911                                 Node currentNode = childList.item(i);
912                                 switch (currentNode.getNodeType()) {
913                                         case Node.TEXT_NODE:
914                                                 break;
915                                         case Node.ELEMENT_NODE:
916                                                 String nodeName = currentNode.getNodeName();
917                                                 if (nodeName.endsWith("lockscope"))
918                                                         lockScopeNode = currentNode;
919                                                 if (nodeName.endsWith("locktype"))
920                                                         lockTypeNode = currentNode;
921                                                 if (nodeName.endsWith("owner"))
922                                                         lockOwnerNode = currentNode;
923                                                 break;
924                                 }
925                         }
926
927                         if (lockScopeNode != null) {
928                                 childList = lockScopeNode.getChildNodes();
929                                 for (int i = 0; i < childList.getLength(); i++) {
930                                         Node currentNode = childList.item(i);
931                                         switch (currentNode.getNodeType()) {
932                                                 case Node.TEXT_NODE:
933                                                         break;
934                                                 case Node.ELEMENT_NODE:
935                                                         String tempScope = currentNode.getNodeName();
936                                                         if (tempScope.indexOf(':') != -1)
937                                                                 lock.scope = tempScope.substring(tempScope.indexOf(':') + 1);
938                                                         else
939                                                                 lock.scope = tempScope;
940                                                         break;
941                                         }
942                                 }
943                                 if (lock.scope == null)
944                                         // Bad request
945                                         resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
946                         } else
947                                 // Bad request
948                                 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
949
950                         if (lockTypeNode != null) {
951                                 childList = lockTypeNode.getChildNodes();
952                                 for (int i = 0; i < childList.getLength(); i++) {
953                                         Node currentNode = childList.item(i);
954                                         switch (currentNode.getNodeType()) {
955                                                 case Node.TEXT_NODE:
956                                                         break;
957                                                 case Node.ELEMENT_NODE:
958                                                         String tempType = currentNode.getNodeName();
959                                                         if (tempType.indexOf(':') != -1)
960                                                                 lock.type = tempType.substring(tempType.indexOf(':') + 1);
961                                                         else
962                                                                 lock.type = tempType;
963                                                         break;
964                                         }
965                                 }
966
967                                 if (lock.type == null)
968                                         // Bad request
969                                         resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
970                         } else
971                                 // Bad request
972                                 resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
973
974                         if (lockOwnerNode != null) {
975                                 childList = lockOwnerNode.getChildNodes();
976                                 for (int i = 0; i < childList.getLength(); i++) {
977                                         Node currentNode = childList.item(i);
978                                         switch (currentNode.getNodeType()) {
979                                                 case Node.TEXT_NODE:
980                                                         lock.owner += currentNode.getNodeValue();
981                                                         break;
982                                                 case Node.ELEMENT_NODE:
983                                                         strWriter = new StringWriter();
984                                                         domWriter = new DOMWriter(strWriter, true);
985                                                         domWriter.setQualifiedNames(false);
986                                                         domWriter.print(currentNode);
987                                                         lock.owner += strWriter.toString();
988                                                         break;
989                                         }
990                                 }
991
992                                 if (lock.owner == null)
993                                         // Bad request
994                                         resp.setStatus(WebdavStatus.SC_BAD_REQUEST);
995                         } else
996                                 lock.owner = new String();
997                 }
998
999                 String path = getRelativePath(req);
1000                 lock.path = path;
1001                 User user = getUser(req);
1002                 boolean exists = true;
1003                 Object object = null;
1004                 try {
1005                         object = getService().getResourceAtPath(user.getId(), path, true);
1006                 } catch (ObjectNotFoundException e) {
1007                         exists = false;
1008                 } catch (RpcException e) {
1009                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1010                         return;
1011                 }
1012
1013                 if (lockRequestType == LOCK_CREATION) {
1014                         // Generating lock id
1015                         String lockTokenStr = req.getServletPath() + "-" + lock.type + "-" + lock.scope + "-" + req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.tokens + "-" + lock.expiresAt + "-" + System.currentTimeMillis() + "-" + secret;
1016                         String lockToken = md5Encoder.encode(md5Helper.digest(lockTokenStr.getBytes()));
1017
1018                         if (exists && object instanceof FolderDTO && lock.depth == INFINITY)
1019                                 // Locking a collection (and all its member resources)
1020                                 lock.tokens.addElement(lockToken);
1021                         else {
1022                                 // Locking a single resource
1023                                 lock.tokens.addElement(lockToken);
1024                                 // Add the Lock-Token header as by RFC 2518 8.10.1
1025                                 // - only do this for newly created locks
1026                                 resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">");
1027
1028                         }
1029                 }
1030
1031                 if (lockRequestType == LOCK_REFRESH) {
1032
1033                 }
1034
1035                 // Set the status, then generate the XML response containing
1036                 // the lock information.
1037                 XMLWriter generatedXML = new XMLWriter();
1038                 generatedXML.writeXMLHeader();
1039                 generatedXML.writeElement(null, "prop" + generateNamespaceDeclarations(), XMLWriter.OPENING);
1040                 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.OPENING);
1041                 lock.toXML(generatedXML);
1042                 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.CLOSING);
1043                 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1044
1045                 resp.setStatus(WebdavStatus.SC_OK);
1046                 resp.setContentType("text/xml; charset=UTF-8");
1047                 Writer writer = resp.getWriter();
1048                 writer.write(generatedXML.toString());
1049                 writer.close();
1050         }
1051
1052         /**
1053          * The MOVE method.
1054          *
1055          * @param req the HTTP request
1056          * @param resp the HTTP response
1057          * @throws IOException if an error occurs while sending the response
1058          * @throws ServletException
1059          */
1060         private void doMove(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1061                 if (isLocked(req)) {
1062                         resp.sendError(WebdavStatus.SC_LOCKED);
1063                         return;
1064                 }
1065
1066                 String path = getRelativePath(req);
1067
1068                 if (copyResource(req, resp))
1069                         deleteResource(path, req, resp, false);
1070         }
1071
1072         /**
1073          * The COPY method.
1074          *
1075          * @param req the HTTP request
1076          * @param resp the HTTP response
1077          * @throws IOException if an error occurs while sending the response
1078          * @throws ServletException
1079          */
1080         private void doCopy(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1081                 copyResource(req, resp);
1082         }
1083
1084         /**
1085          * The MKCOL method.
1086          *
1087          * @param req the HTTP request
1088          * @param resp the HTTP response
1089          * @throws IOException if an error occurs while sending the response
1090          * @throws ServletException
1091          */
1092         private void doMkcol(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
1093                 if (isLocked(req)) {
1094                         resp.sendError(WebdavStatus.SC_LOCKED);
1095                         return;
1096                 }
1097                 final String path = getRelativePath(req);
1098                 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
1099                         resp.sendError(WebdavStatus.SC_FORBIDDEN);
1100                         return;
1101                 }
1102
1103                 final User user = getUser(req);
1104                 boolean exists = true;
1105                 try {
1106                         getService().getResourceAtPath(user.getId(), path, true);
1107                 } catch (ObjectNotFoundException e) {
1108                         exists = false;
1109                 } catch (RpcException e) {
1110                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1111                         return;
1112                 }
1113
1114                 // Can't create a collection if a resource already exists at the given
1115                 // path.
1116                 if (exists) {
1117                         // Get allowed methods.
1118                         StringBuffer methodsAllowed;
1119                         try {
1120                                 methodsAllowed = determineMethodsAllowed(req);
1121                         } catch (RpcException e) {
1122                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1123                                 return;
1124                         }
1125                         resp.addHeader("Allow", methodsAllowed.toString());
1126                         resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
1127                         return;
1128                 }
1129
1130                 if (req.getInputStream().available() > 0) {
1131                         DocumentBuilder documentBuilder = getDocumentBuilder();
1132                         try {
1133                                 Document document = documentBuilder.parse(new InputSource(req.getInputStream()));
1134                                 // TODO : Process this request body
1135                                 resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED);
1136                                 return;
1137                         } catch (SAXException saxe) {
1138                                 // Parse error - assume invalid content
1139                                 resp.sendError(WebdavStatus.SC_BAD_REQUEST);
1140                                 return;
1141                         }
1142                 }
1143
1144                 Object parent;
1145                 try {
1146                         parent = getService().getResourceAtPath(user.getId(), getParentPath(path), true);
1147                 } catch (ObjectNotFoundException e1) {
1148                         resp.sendError(WebdavStatus.SC_CONFLICT, WebdavStatus.getStatusText(WebdavStatus.SC_CONFLICT));
1149                         return;
1150                 } catch (RpcException e1) {
1151                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1152                         return;
1153                 }
1154                 try {
1155                         if (parent instanceof FolderDTO) {
1156                                 final FolderDTO folder = (FolderDTO) parent;
1157                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1158                                         @Override
1159                                         public Void call() throws Exception {
1160                                                 getService().createFolder(user.getId(), folder.getId(), getLastElement(path));
1161                                                 return null;
1162                                         }
1163                                 });
1164                         } else {
1165                                 resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1166                                 return;
1167                         }
1168                 } catch (DuplicateNameException e) {
1169                         // XXX If the existing name is a folder we should be returning
1170                         // SC_METHOD_NOT_ALLOWED, or even better, just do the createFolder
1171                         // without checking first and then deal with the exceptions.
1172                         resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1173                         return;
1174                 } catch (InsufficientPermissionsException e) {
1175                         resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN));
1176                         return;
1177                 } catch (ObjectNotFoundException e) {
1178                         resp.sendError(WebdavStatus.SC_CONFLICT, WebdavStatus.getStatusText(WebdavStatus.SC_CONFLICT));
1179                         return;
1180                 } catch (RpcException e) {
1181                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1182                         return;
1183                 } catch (Exception e) {
1184                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1185                         return;
1186                 }
1187                 resp.setStatus(WebdavStatus.SC_CREATED);
1188         }
1189
1190         /**
1191          * For a provided path, remove the last element and return the rest, that is
1192          * the path of the parent folder.
1193          *
1194          * @param path the specified path
1195          * @return the path of the parent folder
1196          * @throws ObjectNotFoundException if the provided string contains no path
1197          *             delimiters
1198          */
1199         protected String getParentPath(String path) throws ObjectNotFoundException {
1200                 int lastDelimiter = path.lastIndexOf('/');
1201                 if (lastDelimiter == 0)
1202                         return "/";
1203                 if (lastDelimiter == -1)
1204                         // No path found.
1205                         throw new ObjectNotFoundException("There is no parent in the path: " + path);
1206                 else if (lastDelimiter < path.length() - 1)
1207                         // Return the part before the delimiter.
1208                         return path.substring(0, lastDelimiter);
1209                 else {
1210                         // Remove the trailing delimiter and then recurse.
1211                         String strippedTrail = path.substring(0, lastDelimiter);
1212                         return getParentPath(strippedTrail);
1213                 }
1214         }
1215
1216         /**
1217          * Get the last element in a path that denotes the file or folder name.
1218          *
1219          * @param path the provided path
1220          * @return the last element in the path
1221          */
1222         protected String getLastElement(String path) {
1223                 int lastDelimiter = path.lastIndexOf('/');
1224                 if (lastDelimiter == -1)
1225                         // No path found.
1226                         return path;
1227                 else if (lastDelimiter < path.length() - 1)
1228                         // Return the part after the delimiter.
1229                         return path.substring(lastDelimiter + 1);
1230                 else {
1231                         // Remove the trailing delimiter and then recurse.
1232                         String strippedTrail = path.substring(0, lastDelimiter);
1233                         return getLastElement(strippedTrail);
1234                 }
1235         }
1236
1237         /**
1238          * Only use the PathInfo for determining the requested path. If the
1239          * ServletPath is non-null, it will be because the WebDAV servlet has been
1240          * mapped to a URL other than /* to configure editing at different URL than
1241          * normal viewing.
1242          *
1243          * @param request the servlet request we are processing
1244          * @return the relative path
1245          * @throws UnsupportedEncodingException
1246          */
1247         protected String getRelativePath(HttpServletRequest request) {
1248                 // Remove the servlet path from the request URI.
1249                 String p = request.getRequestURI();
1250                 String servletPath = request.getContextPath() + request.getServletPath();
1251                 String result = p.substring(servletPath.length());
1252                 try {
1253                         result = URLDecoder.decode(result, "UTF-8");
1254                 } catch (UnsupportedEncodingException e) {
1255                 }
1256                 if (result == null || result.equals(""))
1257                         result = "/";
1258                 return result;
1259
1260         }
1261
1262         /**
1263          * Return JAXP document builder instance.
1264          *
1265          * @return the DocumentBuilder
1266          * @throws ServletException
1267          */
1268         private DocumentBuilder getDocumentBuilder() throws ServletException {
1269                 DocumentBuilder documentBuilder = null;
1270                 DocumentBuilderFactory documentBuilderFactory = null;
1271                 try {
1272                         documentBuilderFactory = DocumentBuilderFactory.newInstance();
1273                         documentBuilderFactory.setNamespaceAware(true);
1274                         documentBuilderFactory.setExpandEntityReferences(false);
1275                         documentBuilder = documentBuilderFactory.newDocumentBuilder();
1276                         documentBuilder.setEntityResolver(new WebdavResolver(getServletContext()));
1277                 } catch (ParserConfigurationException e) {
1278                         throw new ServletException("Error while creating a document builder");
1279                 }
1280                 return documentBuilder;
1281         }
1282
1283         /**
1284          * Generate the namespace declarations.
1285          *
1286          * @return the namespace declarations
1287          */
1288         private String generateNamespaceDeclarations() {
1289                 return " xmlns=\"" + DEFAULT_NAMESPACE + "\"";
1290         }
1291
1292         /**
1293          * Propfind helper method. Dispays the properties of a lock-null resource.
1294          *
1295          * @param req the HTTP request
1296          * @param resources Resources object associated with this context
1297          * @param generatedXML XML response to the Propfind request
1298          * @param path Path of the current resource
1299          * @param type Propfind type
1300          * @param propertiesVector If the propfind type is find properties by name,
1301          *            then this Vector contains those properties
1302          */
1303         @SuppressWarnings("unused")
1304         private void parseLockNullProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, Vector propertiesVector) {
1305                 return;
1306         }
1307
1308         /**
1309          * Propfind helper method.
1310          *
1311          * @param req The servlet request
1312          * @param resources Resources object associated with this context
1313          * @param generatedXML XML response to the Propfind request
1314          * @param path Path of the current resource
1315          * @param type Propfind type
1316          * @param propertiesVector If the propfind type is find properties by name,
1317          *            then this Vector contains those properties
1318          * @param resource the resource object
1319          */
1320         private void parseProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, Vector<String> propertiesVector, Object resource) {
1321
1322                 // Exclude any resource in the /WEB-INF and /META-INF subdirectories
1323                 // (the "toUpperCase()" avoids problems on Windows systems)
1324                 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF"))
1325                         return;
1326
1327                 FolderDTO folder = null;
1328                 FileHeaderDTO file = null;
1329                 if (resource instanceof FolderDTO)
1330                         folder = (FolderDTO) resource;
1331                 else
1332                         file = (FileHeaderDTO) resource;
1333                 // Retrieve the creation date.
1334                 long creation = 0;
1335                 if (folder != null)
1336                         creation = folder.getAuditInfo().getCreationDate().getTime();
1337                 else
1338                         creation = file.getAuditInfo().getCreationDate().getTime();
1339                 // Retrieve the modification date.
1340                 long modification = 0;
1341                 if (folder != null)
1342                         modification = folder.getAuditInfo().getCreationDate().getTime();
1343                 else
1344                         modification = file.getAuditInfo().getCreationDate().getTime();
1345
1346                 generatedXML.writeElement(null, "response", XMLWriter.OPENING);
1347                 String status = new String("HTTP/1.1 " + WebdavStatus.SC_OK + " " + WebdavStatus.getStatusText(WebdavStatus.SC_OK));
1348
1349                 // Generating href element
1350                 generatedXML.writeElement(null, "href", XMLWriter.OPENING);
1351
1352                 String href = req.getContextPath() + req.getServletPath();
1353                 if (href.endsWith("/") && path.startsWith("/"))
1354                         href += path.substring(1);
1355                 else
1356                         href += path;
1357                 if (folder != null && !href.endsWith("/"))
1358                         href += "/";
1359
1360                 generatedXML.writeText(rewriteUrl(href));
1361
1362                 generatedXML.writeElement(null, "href", XMLWriter.CLOSING);
1363
1364                 String resourceName = path;
1365                 int lastSlash = path.lastIndexOf('/');
1366                 if (lastSlash != -1)
1367                         resourceName = resourceName.substring(lastSlash + 1);
1368
1369                 switch (type) {
1370
1371                         case FIND_ALL_PROP:
1372
1373                                 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1374                                 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1375
1376                                 generatedXML.writeProperty(null, "creationdate", getISOCreationDate(creation));
1377                                 generatedXML.writeElement(null, "displayname", XMLWriter.OPENING);
1378                                 generatedXML.writeData(resourceName);
1379                                 generatedXML.writeElement(null, "displayname", XMLWriter.CLOSING);
1380                                 if (file != null) {
1381                                         generatedXML.writeProperty(null, "getlastmodified", FastHttpDateFormat.formatDate(modification, null));
1382                                         generatedXML.writeProperty(null, "getcontentlength", String.valueOf(file.getFileSize()));
1383                                         String contentType = file.getMimeType();
1384                                         if (contentType != null)
1385                                                 generatedXML.writeProperty(null, "getcontenttype", contentType);
1386                                         generatedXML.writeProperty(null, "getetag", getETag(file, null));
1387                                         generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1388                                 } else {
1389                                         generatedXML.writeElement(null, "resourcetype", XMLWriter.OPENING);
1390                                         generatedXML.writeElement(null, "collection", XMLWriter.NO_CONTENT);
1391                                         generatedXML.writeElement(null, "resourcetype", XMLWriter.CLOSING);
1392                                 }
1393
1394                                 generatedXML.writeProperty(null, "source", "");
1395
1396                                 String supportedLocks = "<lockentry>" + "<lockscope><exclusive/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>" + "<lockentry>" + "<lockscope><shared/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>";
1397                                 generatedXML.writeElement(null, "supportedlock", XMLWriter.OPENING);
1398                                 generatedXML.writeText(supportedLocks);
1399                                 generatedXML.writeElement(null, "supportedlock", XMLWriter.CLOSING);
1400
1401                                 generateLockDiscovery(path, generatedXML);
1402
1403                                 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1404                                 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1405                                 generatedXML.writeText(status);
1406                                 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1407                                 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1408
1409                                 break;
1410
1411                         case FIND_PROPERTY_NAMES:
1412
1413                                 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1414                                 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1415
1416                                 generatedXML.writeElement(null, "creationdate", XMLWriter.NO_CONTENT);
1417                                 generatedXML.writeElement(null, "displayname", XMLWriter.NO_CONTENT);
1418                                 if (file != null) {
1419                                         generatedXML.writeElement(null, "getcontentlanguage", XMLWriter.NO_CONTENT);
1420                                         generatedXML.writeElement(null, "getcontentlength", XMLWriter.NO_CONTENT);
1421                                         generatedXML.writeElement(null, "getcontenttype", XMLWriter.NO_CONTENT);
1422                                         generatedXML.writeElement(null, "getetag", XMLWriter.NO_CONTENT);
1423                                         generatedXML.writeElement(null, "getlastmodified", XMLWriter.NO_CONTENT);
1424                                 }
1425                                 generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1426                                 generatedXML.writeElement(null, "source", XMLWriter.NO_CONTENT);
1427                                 generatedXML.writeElement(null, "lockdiscovery", XMLWriter.NO_CONTENT);
1428
1429                                 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1430                                 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1431                                 generatedXML.writeText(status);
1432                                 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1433                                 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1434
1435                                 break;
1436
1437                         case FIND_BY_PROPERTY:
1438
1439                                 Vector<String> propertiesNotFound = new Vector<String>();
1440
1441                                 // Parse the list of properties
1442
1443                                 generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1444                                 generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1445
1446                                 Enumeration<String> properties = propertiesVector.elements();
1447
1448                                 while (properties.hasMoreElements()) {
1449
1450                                         String property = properties.nextElement();
1451
1452                                         if (property.equals("creationdate"))
1453                                                 generatedXML.writeProperty(null, "creationdate", getISOCreationDate(creation));
1454                                         else if (property.equals("displayname")) {
1455                                                 generatedXML.writeElement(null, "displayname", XMLWriter.OPENING);
1456                                                 generatedXML.writeData(resourceName);
1457                                                 generatedXML.writeElement(null, "displayname", XMLWriter.CLOSING);
1458                                         } else if (property.equals("getcontentlanguage")) {
1459                                                 if (folder != null)
1460                                                         propertiesNotFound.addElement(property);
1461                                                 else
1462                                                         generatedXML.writeElement(null, "getcontentlanguage", XMLWriter.NO_CONTENT);
1463                                         } else if (property.equals("getcontentlength")) {
1464                                                 if (folder != null)
1465                                                         propertiesNotFound.addElement(property);
1466                                                 else
1467                                                         generatedXML.writeProperty(null, "getcontentlength", String.valueOf(file.getFileSize()));
1468                                         } else if (property.equals("getcontenttype")) {
1469                                                 if (folder != null)
1470                                                         propertiesNotFound.addElement(property);
1471                                                 else
1472                                                         // XXX Once we properly store the MIME type in the
1473                                                         // file,
1474                                                         // retrieve it from there.
1475                                                         generatedXML.writeProperty(null, "getcontenttype", getServletContext().getMimeType(file.getName()));
1476                                         } else if (property.equals("getetag")) {
1477                                                 if (folder != null)
1478                                                         propertiesNotFound.addElement(property);
1479                                                 else
1480                                                         generatedXML.writeProperty(null, "getetag", getETag(file, null));
1481                                         } else if (property.equals("getlastmodified")) {
1482                                                 if (folder != null)
1483                                                         propertiesNotFound.addElement(property);
1484                                                 else
1485                                                         generatedXML.writeProperty(null, "getlastmodified", FastHttpDateFormat.formatDate(modification, null));
1486                                         } else if (property.equals("resourcetype")) {
1487                                                 if (folder != null) {
1488                                                         generatedXML.writeElement(null, "resourcetype", XMLWriter.OPENING);
1489                                                         generatedXML.writeElement(null, "collection", XMLWriter.NO_CONTENT);
1490                                                         generatedXML.writeElement(null, "resourcetype", XMLWriter.CLOSING);
1491                                                 } else
1492                                                         generatedXML.writeElement(null, "resourcetype", XMLWriter.NO_CONTENT);
1493                                         } else if (property.equals("source"))
1494                                                 generatedXML.writeProperty(null, "source", "");
1495                                         else if (property.equals("supportedlock")) {
1496                                                 supportedLocks = "<lockentry>" + "<lockscope><exclusive/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>" + "<lockentry>" + "<lockscope><shared/></lockscope>" + "<locktype><write/></locktype>" + "</lockentry>";
1497                                                 generatedXML.writeElement(null, "supportedlock", XMLWriter.OPENING);
1498                                                 generatedXML.writeText(supportedLocks);
1499                                                 generatedXML.writeElement(null, "supportedlock", XMLWriter.CLOSING);
1500                                         } else if (property.equals("lockdiscovery")) {
1501                                                 if (!generateLockDiscovery(path, generatedXML))
1502                                                         propertiesNotFound.addElement(property);
1503                                         } else
1504                                                 propertiesNotFound.addElement(property);
1505                                 }
1506
1507                                 generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1508                                 generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1509                                 generatedXML.writeText(status);
1510                                 generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1511                                 generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1512
1513                                 Enumeration propertiesNotFoundList = propertiesNotFound.elements();
1514
1515                                 if (propertiesNotFoundList.hasMoreElements()) {
1516
1517                                         status = new String("HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + " " + WebdavStatus.getStatusText(WebdavStatus.SC_NOT_FOUND));
1518
1519                                         generatedXML.writeElement(null, "propstat", XMLWriter.OPENING);
1520                                         generatedXML.writeElement(null, "prop", XMLWriter.OPENING);
1521
1522                                         while (propertiesNotFoundList.hasMoreElements())
1523                                                 generatedXML.writeElement(null, (String) propertiesNotFoundList.nextElement(), XMLWriter.NO_CONTENT);
1524                                         generatedXML.writeElement(null, "prop", XMLWriter.CLOSING);
1525                                         generatedXML.writeElement(null, "status", XMLWriter.OPENING);
1526                                         generatedXML.writeText(status);
1527                                         generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
1528                                         generatedXML.writeElement(null, "propstat", XMLWriter.CLOSING);
1529                                 }
1530
1531                                 break;
1532
1533                 }
1534
1535                 generatedXML.writeElement(null, "response", XMLWriter.CLOSING);
1536
1537         }
1538
1539         /**
1540          * Get the ETag associated with a file.
1541          *
1542          * @param file the FileHeaderDTO object for this file
1543          * @param oldBody the old version of the file, if requested
1544          * @return a string containing the ETag
1545          */
1546         protected String getETag(FileHeaderDTO file, FileBodyDTO oldBody) {
1547                 if (oldBody == null)
1548                         return "\"" + file.getFileSize() + "-" + file.getAuditInfo().getModificationDate().getTime() + "\"";
1549                 return "\"" + oldBody.getFileSize() + "-" + oldBody.getAuditInfo().getModificationDate().getTime() + "\"";
1550         }
1551
1552         /**
1553          * URL rewriter.
1554          *
1555          * @param path Path which has to be rewritten
1556          * @return the rewritten URL
1557          */
1558         private String rewriteUrl(String path) {
1559                 return urlEncoder.encode(path);
1560         }
1561
1562         /**
1563          * Print the lock discovery information associated with a path.
1564          *
1565          * @param path Path
1566          * @param generatedXML XML data to which the locks info will be appended
1567          * @return true if at least one lock was displayed
1568          */
1569         @SuppressWarnings("unused")
1570         private boolean generateLockDiscovery(String path,  XMLWriter generatedXML) {
1571                         return false;
1572         }
1573
1574         /**
1575          * Get creation date in ISO format.
1576          *
1577          * @param creationDate
1578          * @return the formatted date
1579          */
1580         private String getISOCreationDate(long creationDate) {
1581                 String dateValue = null;
1582                 synchronized (creationDateFormat) {
1583                         dateValue = creationDateFormat.format(new Date(creationDate));
1584                 }
1585                 StringBuffer creationDateValue = new StringBuffer(dateValue);
1586                 /*
1587                 int offset = Calendar.getInstance().getTimeZone().getRawOffset()
1588                     / 3600000; // FIXME ?
1589                 if (offset < 0) {
1590                     creationDateValue.append("-");
1591                     offset = -offset;
1592                 } else if (offset > 0) {
1593                     creationDateValue.append("+");
1594                 }
1595                 if (offset != 0) {
1596                     if (offset < 10)
1597                         creationDateValue.append("0");
1598                     creationDateValue.append(offset + ":00");
1599                 } else {
1600                     creationDateValue.append("Z");
1601                 }
1602                  */
1603                 return creationDateValue.toString();
1604         }
1605
1606         /**
1607          * Determines the methods normally allowed for the resource.
1608          *
1609          * @param req the HTTP request
1610          * @return a list of the allowed methods
1611          * @throws RpcException if there is an error while communicating with the
1612          *             backend
1613          */
1614         private StringBuffer determineMethodsAllowed(HttpServletRequest req) throws RpcException {
1615                 StringBuffer methodsAllowed = new StringBuffer();
1616                 boolean exists = true;
1617                 Object object = null;
1618                 User user = getUser(req);
1619                 String path = getRelativePath(req);
1620                 if (user == null && "/".equals(path))
1621                         // Special case: OPTIONS request before authentication
1622                         return new StringBuffer("OPTIONS, GET, HEAD, POST, DELETE, TRACE, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, PROPFIND, PUT");
1623                 try {
1624                         object = getService().getResourceAtPath(user.getId(), path, true);
1625                 } catch (ObjectNotFoundException e) {
1626                         exists = false;
1627                 }
1628
1629                 if (!exists) {
1630                         methodsAllowed.append("OPTIONS, MKCOL, PUT, LOCK");
1631                         return methodsAllowed;
1632                 }
1633
1634                 methodsAllowed.append("OPTIONS, GET, HEAD, POST, DELETE, TRACE");
1635                 methodsAllowed.append(", PROPPATCH, COPY, MOVE, LOCK, UNLOCK");
1636                 methodsAllowed.append(", PROPFIND");
1637
1638                 if (!(object instanceof FolderDTO))
1639                         methodsAllowed.append(", PUT");
1640
1641                 return methodsAllowed;
1642         }
1643
1644         /**
1645          * Check to see if a resource is currently write locked. The method will
1646          * look at the "If" header to make sure the client has given the appropriate
1647          * lock tokens.
1648          *
1649          * @param req the HTTP request
1650          * @return boolean true if the resource is locked (and no appropriate lock
1651          *         token has been found for at least one of the non-shared locks
1652          *         which are present on the resource).
1653          */
1654         private boolean isLocked(@SuppressWarnings("unused") HttpServletRequest req) {
1655                 return false;
1656         }
1657
1658         /**
1659          * Check to see if a resource is currently write locked.
1660          *
1661          * @param path Path of the resource
1662          * @param ifHeader "If" HTTP header which was included in the request
1663          * @return boolean true if the resource is locked (and no appropriate lock
1664          *         token has been found for at least one of the non-shared locks
1665          *         which are present on the resource).
1666          */
1667         private boolean isLocked(@SuppressWarnings("unused") String path, @SuppressWarnings("unused") String ifHeader) {
1668                 return false;
1669         }
1670
1671         /**
1672          * Parse the content-range header.
1673          *
1674          * @param request The servlet request we are processing
1675          * @param response The servlet response we are creating
1676          * @return Range
1677          * @throws IOException
1678          */
1679         protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response) throws IOException {
1680
1681                 // Retrieving the content-range header (if any is specified
1682                 String rangeHeader = request.getHeader("Content-Range");
1683
1684                 if (rangeHeader == null)
1685                         return null;
1686
1687                 // bytes is the only range unit supported
1688                 if (!rangeHeader.startsWith("bytes")) {
1689                         response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1690                         return null;
1691                 }
1692
1693                 rangeHeader = rangeHeader.substring(6).trim();
1694
1695                 int dashPos = rangeHeader.indexOf('-');
1696                 int slashPos = rangeHeader.indexOf('/');
1697
1698                 if (dashPos == -1) {
1699                         response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1700                         return null;
1701                 }
1702
1703                 if (slashPos == -1) {
1704                         response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1705                         return null;
1706                 }
1707
1708                 Range range = new Range();
1709
1710                 try {
1711                         range.start = Long.parseLong(rangeHeader.substring(0, dashPos));
1712                         range.end = Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos));
1713                         range.length = Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length()));
1714                 } catch (NumberFormatException e) {
1715                         response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1716                         return null;
1717                 }
1718
1719                 if (!range.validate()) {
1720                         response.sendError(HttpServletResponse.SC_BAD_REQUEST);
1721                         return null;
1722                 }
1723
1724                 return range;
1725
1726         }
1727
1728         /**
1729          * Handle a partial PUT. New content specified in request is appended to
1730          * existing content in oldRevisionContent (if present). This code does not
1731          * support simultaneous partial updates to the same resource.
1732          *
1733          * @param req
1734          * @param range
1735          * @param path
1736          * @return
1737          * @throws IOException
1738          * @throws RpcException
1739          * @throws InsufficientPermissionsException
1740          * @throws ObjectNotFoundException
1741          */
1742         protected File executePartialPut(HttpServletRequest req, Range range, String path) throws IOException, RpcException, ObjectNotFoundException, InsufficientPermissionsException {
1743                 // Append data specified in ranges to existing content for this
1744                 // resource - create a temporary file on the local file system to
1745                 // perform this operation.
1746                 File tempDir = (File) getServletContext().getAttribute("javax.servlet.context.tempdir");
1747                 // Convert all '/' characters to '.' in resourcePath
1748                 String convertedResourcePath = path.replace('/', '.');
1749                 File contentFile = new File(tempDir, convertedResourcePath);
1750                 if (contentFile.createNewFile())
1751                         // Clean up contentFile when Tomcat is terminated.
1752                         contentFile.deleteOnExit();
1753
1754                 RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw");
1755
1756                 User user = getUser(req);
1757                 User owner = getOwner(req);
1758                 FileHeaderDTO oldResource = null;
1759                 try {
1760                         Object obj = getService().getResourceAtPath(owner.getId(), path, true);
1761                         if (obj instanceof FileHeaderDTO)
1762                                 oldResource = (FileHeaderDTO) obj;
1763                 } catch (ObjectNotFoundException e) {
1764                         // Do nothing.
1765                 }
1766
1767                 // Copy data in oldRevisionContent to contentFile
1768                 if (oldResource != null) {
1769                         InputStream contents = getService().getFileContents(user.getId(), oldResource.getId());
1770                         BufferedInputStream bufOldRevStream = new BufferedInputStream(contents, BUFFER_SIZE);
1771
1772                         int numBytesRead;
1773                         byte[] copyBuffer = new byte[BUFFER_SIZE];
1774                         while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1)
1775                                 randAccessContentFile.write(copyBuffer, 0, numBytesRead);
1776
1777                         bufOldRevStream.close();
1778                 }
1779
1780                 randAccessContentFile.setLength(range.length);
1781
1782                 // Append data in request input stream to contentFile
1783                 randAccessContentFile.seek(range.start);
1784                 int numBytesRead;
1785                 byte[] transferBuffer = new byte[BUFFER_SIZE];
1786                 BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE);
1787                 while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1)
1788                         randAccessContentFile.write(transferBuffer, 0, numBytesRead);
1789                 randAccessContentFile.close();
1790                 requestBufInStream.close();
1791
1792                 return contentFile;
1793
1794         }
1795
1796         /**
1797          * Serve the specified resource, optionally including the data content.
1798          *
1799          * @param req The servlet request we are processing
1800          * @param resp The servlet response we are creating
1801          * @param content Should the content be included?
1802          * @exception IOException if an input/output error occurs
1803          * @exception ServletException if a servlet-specified error occurs
1804          * @throws RpcException
1805          * @throws InsufficientPermissionsException
1806          * @throws ObjectNotFoundException
1807          */
1808         protected void serveResource(HttpServletRequest req, HttpServletResponse resp, boolean content) throws IOException, ServletException, ObjectNotFoundException, InsufficientPermissionsException, RpcException {
1809
1810                 // Identify the requested resource path
1811                 String path = getRelativePath(req);
1812                 if (logger.isDebugEnabled())
1813                         if (content)
1814                                 logger.debug("Serving resource '" + path + "' headers and data");
1815                         else
1816                                 logger.debug("Serving resource '" + path + "' headers only");
1817
1818                 User user = getUser(req);
1819                 boolean exists = true;
1820                 Object resource = null;
1821                 FileHeaderDTO file = null;
1822                 FolderDTO folder = null;
1823                 try {
1824                         resource = getService().getResourceAtPath(user.getId(), path, true);
1825                 } catch (ObjectNotFoundException e) {
1826                         exists = false;
1827                 } catch (RpcException e) {
1828                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1829                         return;
1830                 }
1831
1832                 if (!exists) {
1833                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
1834                         return;
1835                 }
1836
1837                 if (resource instanceof FolderDTO)
1838                         folder = (FolderDTO) resource;
1839                 else
1840                         file = (FileHeaderDTO) resource;
1841
1842                 // If the resource is not a collection, and the resource path
1843                 // ends with "/" or "\", return NOT FOUND
1844                 if (folder == null)
1845                         if (path.endsWith("/") || path.endsWith("\\")) {
1846                                 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
1847                                 return;
1848                         }
1849
1850                 // Check if the conditions specified in the optional If headers are
1851                 // satisfied.
1852                 if (folder == null)
1853                         // Checking If headers
1854                         if (!checkIfHeaders(req, resp, file, null))
1855                                 return;
1856
1857                 // Find content type.
1858                 String contentType = null;
1859                 if (file != null) {
1860                         contentType = file.getMimeType();
1861                         if (contentType == null) {
1862                                 contentType = getServletContext().getMimeType(file.getName());
1863                                 file.setMimeType(contentType);
1864                         }
1865                 } else
1866                         contentType = "text/html;charset=UTF-8";
1867
1868                 ArrayList ranges = null;
1869                 long contentLength = -1L;
1870
1871                 if (file != null) {
1872                         // Accept ranges header
1873                         resp.setHeader("Accept-Ranges", "bytes");
1874                         // Parse range specifier
1875                         ranges = parseRange(req, resp, file, null);
1876                         // ETag header
1877                         resp.setHeader("ETag", getETag(file, null));
1878                         // Last-Modified header
1879                         resp.setHeader("Last-Modified", getLastModifiedHttp(file.getAuditInfo()));
1880                         // Get content length
1881                         contentLength = file.getFileSize();
1882                         // Special case for zero length files, which would cause a
1883                         // (silent) ISE when setting the output buffer size
1884                         if (contentLength == 0L)
1885                                 content = false;
1886                 }
1887
1888                 ServletOutputStream ostream = null;
1889                 PrintWriter writer = null;
1890
1891                 if (content)
1892                         try {
1893                                 ostream = resp.getOutputStream();
1894                         } catch (IllegalStateException e) {
1895                                 // If it fails, we try to get a Writer instead if we're
1896                                 // trying to serve a text file
1897                                 if (contentType == null || contentType.startsWith("text") || contentType.endsWith("xml"))
1898                                         writer = resp.getWriter();
1899                                 else
1900                                         throw e;
1901                         }
1902
1903                 if (folder != null || (ranges == null || ranges.isEmpty()) && req.getHeader("Range") == null || ranges == FULL) {
1904                         // Set the appropriate output headers
1905                         if (contentType != null) {
1906                                 if (logger.isDebugEnabled())
1907                                         logger.debug("DefaultServlet.serveFile:  contentType='" + contentType + "'");
1908                                 resp.setContentType(contentType);
1909                         }
1910                         if (file != null && contentLength >= 0) {
1911                                 if (logger.isDebugEnabled())
1912                                         logger.debug("DefaultServlet.serveFile:  contentLength=" + contentLength);
1913                                 if (contentLength < Integer.MAX_VALUE)
1914                                         resp.setContentLength((int) contentLength);
1915                                 else
1916                                         // Set the content-length as String to be able to use a long
1917                                         resp.setHeader("content-length", "" + contentLength);
1918                         }
1919
1920                         InputStream renderResult = null;
1921                         if (folder != null)
1922                                 if (content)
1923                                         // Serve the directory browser
1924                                         renderResult = renderHtml(req.getContextPath(), path, folder, req);
1925
1926                         // Copy the input stream to our output stream (if requested)
1927                         if (content) {
1928                                 try {
1929                                         resp.setBufferSize(output);
1930                                 } catch (IllegalStateException e) {
1931                                         // Silent catch
1932                                 }
1933                                 if (ostream != null)
1934                                         copy(file, renderResult, ostream, req, null);
1935                                 else
1936                                         copy(file, renderResult, writer, req, null);
1937                                 updateAccounting(user, new Date(), contentLength);
1938                         }
1939                 } else {
1940                         if (ranges == null || ranges.isEmpty())
1941                                 return;
1942                         // Partial content response.
1943                         resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
1944
1945                         if (ranges.size() == 1) {
1946                                 Range range = (Range) ranges.get(0);
1947                                 resp.addHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length);
1948                                 long length = range.end - range.start + 1;
1949                                 if (length < Integer.MAX_VALUE)
1950                                         resp.setContentLength((int) length);
1951                                 else
1952                                         // Set the content-length as String to be able to use a long
1953                                         resp.setHeader("content-length", "" + length);
1954
1955                                 if (contentType != null) {
1956                                         if (logger.isDebugEnabled())
1957                                                 logger.debug("DefaultServlet.serveFile:  contentType='" + contentType + "'");
1958                                         resp.setContentType(contentType);
1959                                 }
1960
1961                                 if (content) {
1962                                         try {
1963                                                 resp.setBufferSize(output);
1964                                         } catch (IllegalStateException e) {
1965                                                 // Silent catch
1966                                         }
1967                                         if (ostream != null)
1968                                                 copy(file, ostream, range, req, null);
1969                                         else
1970                                                 copy(file, writer, range, req, null);
1971                                         updateAccounting(user, new Date(), contentLength);
1972                                 }
1973
1974                         } else {
1975
1976                                 resp.setContentType("multipart/byteranges; boundary=" + mimeSeparation);
1977
1978                                 if (content) {
1979                                         try {
1980                                                 resp.setBufferSize(output);
1981                                         } catch (IllegalStateException e) {
1982                                                 // Silent catch
1983                                         }
1984                                         if (ostream != null)
1985                                                 copy(file, ostream, ranges.iterator(), contentType, req, null);
1986                                         else
1987                                                 copy(file, writer, ranges.iterator(), contentType, req, null);
1988                                 }
1989
1990                         }
1991
1992                 }
1993
1994         }
1995
1996         /**
1997          * Retrieve the last modified date of a resource in HTTP format.
1998          *
1999          * @param auditInfo the audit info for the specified resource
2000          * @return the last modified date in HTTP format
2001          */
2002         protected String getLastModifiedHttp(AuditInfoDTO auditInfo) {
2003                 Date modifiedDate = auditInfo.getModificationDate();
2004                 if (modifiedDate == null)
2005                         modifiedDate = auditInfo.getCreationDate();
2006                 if (modifiedDate == null)
2007                         modifiedDate = new Date();
2008                 String lastModifiedHttp = null;
2009                 synchronized (format) {
2010                         lastModifiedHttp = format.format(modifiedDate);
2011                 }
2012                 return lastModifiedHttp;
2013         }
2014
2015         /**
2016          * Parse the range header.
2017          *
2018          * @param request The servlet request we are processing
2019          * @param response The servlet response we are creating
2020          * @param file
2021          * @param oldBody the old version of the file, if requested
2022          * @return Vector of ranges
2023          * @throws IOException
2024          */
2025         protected ArrayList parseRange(HttpServletRequest request, HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
2026                 // Checking If-Range
2027                 String headerValue = request.getHeader("If-Range");
2028                 if (headerValue != null) {
2029                         long headerValueTime = -1L;
2030                         try {
2031                                 headerValueTime = request.getDateHeader("If-Range");
2032                         } catch (IllegalArgumentException e) {
2033                                 // Do nothing.
2034                         }
2035
2036                         String eTag = getETag(file, oldBody);
2037                         long lastModified = oldBody == null ?
2038                                                 file.getAuditInfo().getModificationDate().getTime() :
2039                                                 oldBody.getAuditInfo().getModificationDate().getTime();
2040
2041                         if (headerValueTime == -1L) {
2042                                 // If the ETag the client gave does not match the entity
2043                                 // etag, then the entire entity is returned.
2044                                 if (!eTag.equals(headerValue.trim()))
2045                                         return FULL;
2046                         } else
2047                         // If the timestamp of the entity the client got is older than
2048                         // the last modification date of the entity, the entire entity
2049                         // is returned.
2050                         if (lastModified > headerValueTime + 1000)
2051                                 return FULL;
2052                 }
2053
2054                 long fileLength = oldBody == null ? file.getFileSize() : oldBody.getFileSize();
2055                 if (fileLength == 0)
2056                         return null;
2057
2058                 // Retrieving the range header (if any is specified).
2059                 String rangeHeader = request.getHeader("Range");
2060
2061                 if (rangeHeader == null)
2062                         return null;
2063                 // bytes is the only range unit supported (and I don't see the point
2064                 // of adding new ones).
2065                 if (!rangeHeader.startsWith("bytes")) {
2066                         response.addHeader("Content-Range", "bytes */" + fileLength);
2067                         response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2068                         return null;
2069                 }
2070
2071                 rangeHeader = rangeHeader.substring(6);
2072
2073                 // Vector that will contain all the ranges which are successfully
2074                 // parsed.
2075                 ArrayList<Range> result = new ArrayList<Range>();
2076                 StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
2077                 // Parsing the range list
2078                 while (commaTokenizer.hasMoreTokens()) {
2079                         String rangeDefinition = commaTokenizer.nextToken().trim();
2080
2081                         Range currentRange = new Range();
2082                         currentRange.length = fileLength;
2083
2084                         int dashPos = rangeDefinition.indexOf('-');
2085
2086                         if (dashPos == -1) {
2087                                 response.addHeader("Content-Range", "bytes */" + fileLength);
2088                                 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2089                                 return null;
2090                         }
2091
2092                         if (dashPos == 0)
2093                                 try {
2094                                         long offset = Long.parseLong(rangeDefinition);
2095                                         currentRange.start = fileLength + offset;
2096                                         currentRange.end = fileLength - 1;
2097                                 } catch (NumberFormatException e) {
2098                                         response.addHeader("Content-Range", "bytes */" + fileLength);
2099                                         response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2100                                         return null;
2101                                 }
2102                         else
2103                                 try {
2104                                         currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos));
2105                                         if (dashPos < rangeDefinition.length() - 1)
2106                                                 currentRange.end = Long.parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length()));
2107                                         else
2108                                                 currentRange.end = fileLength - 1;
2109                                 } catch (NumberFormatException e) {
2110                                         response.addHeader("Content-Range", "bytes */" + fileLength);
2111                                         response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2112                                         return null;
2113                                 }
2114
2115                         if (!currentRange.validate()) {
2116                                 response.addHeader("Content-Range", "bytes */" + fileLength);
2117                                 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2118                                 return null;
2119                         }
2120                         result.add(currentRange);
2121                 }
2122                 return result;
2123         }
2124
2125         /**
2126          * Check if the conditions specified in the optional If headers are
2127          * satisfied.
2128          *
2129          * @param request The servlet request we are processing
2130          * @param response The servlet response we are creating
2131          * @param file the file resource against which the checks will be made
2132          * @param oldBody the old version of the file, if requested
2133          * @return boolean true if the resource meets all the specified conditions,
2134          *         and false if any of the conditions is not satisfied, in which
2135          *         case request processing is stopped
2136          * @throws IOException
2137          */
2138         protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response,
2139                                 FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
2140                 // TODO : Checking the WebDAV If header
2141                 return checkIfMatch(request, response, file, oldBody) &&
2142                                 checkIfModifiedSince(request, response, file, oldBody) &&
2143                                 checkIfNoneMatch(request, response, file, oldBody) &&
2144                                 checkIfUnmodifiedSince(request, response, file, oldBody);
2145         }
2146
2147         /**
2148          * Check if the if-match condition is satisfied.
2149          *
2150          * @param request The servlet request we are processing
2151          * @param response The servlet response we are creating
2152          * @param file the file object
2153          * @param oldBody the old version of the file, if requested
2154          * @return boolean true if the resource meets the specified condition, and
2155          *         false if the condition is not satisfied, in which case request
2156          *         processing is stopped
2157          * @throws IOException
2158          */
2159         private boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response,
2160                                 FileHeaderDTO file, FileBodyDTO oldBody) throws IOException {
2161                 String eTag = getETag(file, oldBody);
2162                 String headerValue = request.getHeader("If-Match");
2163                 if (headerValue != null)
2164                         if (headerValue.indexOf('*') == -1) {
2165                                 StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
2166                                 boolean conditionSatisfied = false;
2167                                 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
2168                                         String currentToken = commaTokenizer.nextToken();
2169                                         if (currentToken.trim().equals(eTag))
2170                                                 conditionSatisfied = true;
2171                                 }
2172                                 // If none of the given ETags match, 412 Precodition failed is
2173                                 // sent back.
2174                                 if (!conditionSatisfied) {
2175                                         response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2176                                         return false;
2177                                 }
2178                         }
2179                 return true;
2180         }
2181
2182         /**
2183          * Check if the if-modified-since condition is satisfied.
2184          *
2185          * @param request The servlet request we are processing
2186          * @param response The servlet response we are creating
2187          * @param file the file object
2188          * @param oldBody the old version of the file, if requested
2189          * @return boolean true if the resource meets the specified condition, and
2190          *         false if the condition is not satisfied, in which case request
2191          *         processing is stopped
2192          */
2193         private boolean checkIfModifiedSince(HttpServletRequest request,
2194                                 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody) {
2195                 try {
2196                         long headerValue = request.getDateHeader("If-Modified-Since");
2197                         long lastModified = oldBody == null ?
2198                                                 file.getAuditInfo().getModificationDate().getTime() :
2199                                                 oldBody.getAuditInfo().getModificationDate().getTime();
2200                         if (headerValue != -1)
2201                                 // If an If-None-Match header has been specified, if modified
2202                                 // since is ignored.
2203                                 if (request.getHeader("If-None-Match") == null && lastModified < headerValue + 1000) {
2204                                         // The entity has not been modified since the date
2205                                         // specified by the client. This is not an error case.
2206                                         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
2207                                         response.setHeader("ETag", getETag(file, oldBody));
2208                                         return false;
2209                                 }
2210                 } catch (IllegalArgumentException illegalArgument) {
2211                         return true;
2212                 }
2213                 return true;
2214         }
2215
2216         /**
2217          * Check if the if-none-match condition is satisfied.
2218          *
2219          * @param request The servlet request we are processing
2220          * @param response The servlet response we are creating
2221          * @param file the file object
2222          * @param oldBody the old version of the file, if requested
2223          * @return boolean true if the resource meets the specified condition, and
2224          *         false if the condition is not satisfied, in which case request
2225          *         processing is stopped
2226          * @throws IOException
2227          */
2228         private boolean checkIfNoneMatch(HttpServletRequest request,
2229                                 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody)
2230                         throws IOException {
2231                 String eTag = getETag(file, oldBody);
2232                 String headerValue = request.getHeader("If-None-Match");
2233                 if (headerValue != null) {
2234                         boolean conditionSatisfied = false;
2235                         if (!headerValue.equals("*")) {
2236                                 StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ",");
2237                                 while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) {
2238                                         String currentToken = commaTokenizer.nextToken();
2239                                         if (currentToken.trim().equals(eTag))
2240                                                 conditionSatisfied = true;
2241                                 }
2242                         } else
2243                                 conditionSatisfied = true;
2244                         if (conditionSatisfied) {
2245                                 // For GET and HEAD, we should respond with 304 Not Modified.
2246                                 // For every other method, 412 Precondition Failed is sent
2247                                 // back.
2248                                 if ("GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod())) {
2249                                         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
2250                                         response.setHeader("ETag", getETag(file, oldBody));
2251                                         return false;
2252                                 }
2253                                 response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2254                                 return false;
2255                         }
2256                 }
2257                 return true;
2258         }
2259
2260         /**
2261          * Check if the if-unmodified-since condition is satisfied.
2262          *
2263          * @param request The servlet request we are processing
2264          * @param response The servlet response we are creating
2265          * @param file the file object
2266          * @param oldBody the old version of the file, if requested
2267          * @return boolean true if the resource meets the specified condition, and
2268          *         false if the condition is not satisfied, in which case request
2269          *         processing is stopped
2270          * @throws IOException
2271          */
2272         private boolean checkIfUnmodifiedSince(HttpServletRequest request,
2273                                 HttpServletResponse response, FileHeaderDTO file, FileBodyDTO oldBody)
2274                         throws IOException {
2275                 try {
2276                         long lastModified = oldBody == null ?
2277                                                 file.getAuditInfo().getModificationDate().getTime() :
2278                                                 oldBody.getAuditInfo().getModificationDate().getTime();
2279                         long headerValue = request.getDateHeader("If-Unmodified-Since");
2280                         if (headerValue != -1)
2281                                 if (lastModified >= headerValue + 1000) {
2282                                         // The entity has not been modified since the date
2283                                         // specified by the client. This is not an error case.
2284                                         response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
2285                                         return false;
2286                                 }
2287                 } catch (IllegalArgumentException illegalArgument) {
2288                         return true;
2289                 }
2290                 return true;
2291         }
2292
2293         /**
2294          * Copy the contents of the specified input stream to the specified output
2295          * stream, and ensure that both streams are closed before returning (even in
2296          * the face of an exception).
2297          *
2298          * @param file the file resource
2299          * @param is
2300          * @param ostream The output stream to write to
2301          * @param req the HTTP request
2302          * @param oldBody the old version of the file, if requested
2303          * @exception IOException if an input/output error occurs
2304          * @throws RpcException
2305          * @throws InsufficientPermissionsException
2306          * @throws ObjectNotFoundException
2307          */
2308         protected void copy(FileHeaderDTO file, InputStream is, ServletOutputStream ostream,
2309                                 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2310                                 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2311                 IOException exception = null;
2312                 InputStream resourceInputStream = null;
2313                 User user = getUser(req);
2314                 // Files open for all will not have specified a calling user in the request.
2315                 if (user == null)
2316                         user = getOwner(req);
2317                 if (user == null)
2318                         throw new ObjectNotFoundException("No user or owner specified");
2319                 if (file != null)
2320                         resourceInputStream = oldBody == null ?
2321                                                 getService().getFileContents(user.getId(), file.getId()) :
2322                                                 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2323                 else
2324                         resourceInputStream = is;
2325
2326                 InputStream istream = new BufferedInputStream(resourceInputStream, input);
2327                 // Copy the input stream to the output stream
2328                 exception = copyRange(istream, ostream);
2329                 // Clean up the input stream
2330                 istream.close();
2331                 // Rethrow any exception that has occurred
2332                 if (exception != null)
2333                         throw exception;
2334         }
2335
2336         /**
2337          * Copy the contents of the specified input stream to the specified output
2338          * stream, and ensure that both streams are closed before returning (even in
2339          * the face of an exception).
2340          *
2341          * @param istream The input stream to read from
2342          * @param ostream The output stream to write to
2343          * @return Exception which occurred during processing
2344          */
2345         private IOException copyRange(InputStream istream, ServletOutputStream ostream) {
2346                 // Copy the input stream to the output stream
2347                 IOException exception = null;
2348                 byte buffer[] = new byte[input];
2349                 int len = buffer.length;
2350                 while (true)
2351                         try {
2352                                 len = istream.read(buffer);
2353                                 if (len == -1)
2354                                         break;
2355                                 ostream.write(buffer, 0, len);
2356                         } catch (IOException e) {
2357                                 exception = e;
2358                                 len = -1;
2359                                 break;
2360                         }
2361                 return exception;
2362         }
2363
2364         /**
2365          * Copy the contents of the specified input stream to the specified output
2366          * stream, and ensure that both streams are closed before returning (even in
2367          * the face of an exception).
2368          *
2369          * @param file
2370          * @param is
2371          * @param resourceInfo The resource info
2372          * @param writer The writer to write to
2373          * @param req the HTTP request
2374          * @param oldBody the old version of the file, if requested
2375          * @exception IOException if an input/output error occurs
2376          * @throws RpcException
2377          * @throws InsufficientPermissionsException
2378          * @throws ObjectNotFoundException
2379          */
2380         protected void copy(FileHeaderDTO file, InputStream is, PrintWriter writer,
2381                                 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2382                                 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2383                 IOException exception = null;
2384
2385                 User user = getUser(req);
2386                 InputStream resourceInputStream = null;
2387                 if (file != null)
2388                         resourceInputStream = oldBody == null ?
2389                                                 getService().getFileContents(user.getId(), file.getId()) :
2390                                                 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2391                 else
2392                         resourceInputStream = is;
2393
2394                 Reader reader;
2395                 if (fileEncoding == null)
2396                         reader = new InputStreamReader(resourceInputStream);
2397                 else
2398                         reader = new InputStreamReader(resourceInputStream, fileEncoding);
2399
2400                 // Copy the input stream to the output stream
2401                 exception = copyRange(reader, writer);
2402                 // Clean up the reader
2403                 reader.close();
2404                 // Rethrow any exception that has occurred
2405                 if (exception != null)
2406                         throw exception;
2407         }
2408
2409         /**
2410          * Copy the contents of the specified input stream to the specified output
2411          * stream, and ensure that both streams are closed before returning (even in
2412          * the face of an exception).
2413          *
2414          * @param reader The reader to read from
2415          * @param writer The writer to write to
2416          * @return Exception which occurred during processing
2417          */
2418         private IOException copyRange(Reader reader, PrintWriter writer) {
2419                 // Copy the input stream to the output stream
2420                 IOException exception = null;
2421                 char buffer[] = new char[input];
2422                 int len = buffer.length;
2423                 while (true)
2424                         try {
2425                                 len = reader.read(buffer);
2426                                 if (len == -1)
2427                                         break;
2428                                 writer.write(buffer, 0, len);
2429                         } catch (IOException e) {
2430                                 exception = e;
2431                                 len = -1;
2432                                 break;
2433                         }
2434                 return exception;
2435         }
2436
2437         /**
2438          * Copy the contents of the specified input stream to the specified output
2439          * stream, and ensure that both streams are closed before returning (even in
2440          * the face of an exception).
2441          *
2442          * @param file
2443          * @param writer The writer to write to
2444          * @param ranges Enumeration of the ranges the client wanted to retrieve
2445          * @param contentType Content type of the resource
2446          * @param req the HTTP request
2447          * @param oldBody the old version of the file, if requested
2448          * @exception IOException if an input/output error occurs
2449          * @throws RpcException
2450          * @throws InsufficientPermissionsException
2451          * @throws ObjectNotFoundException
2452          */
2453         protected void copy(FileHeaderDTO file, PrintWriter writer, Iterator ranges,
2454                                 String contentType, HttpServletRequest req, FileBodyDTO oldBody)
2455                         throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2456                 User user = getUser(req);
2457                 IOException exception = null;
2458                 while (exception == null && ranges.hasNext()) {
2459                         InputStream resourceInputStream = oldBody == null ?
2460                                                 getService().getFileContents(user.getId(), file.getId()) :
2461                                                 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2462                         Reader reader;
2463                         if (fileEncoding == null)
2464                                 reader = new InputStreamReader(resourceInputStream);
2465                         else
2466                                 reader = new InputStreamReader(resourceInputStream, fileEncoding);
2467                         Range currentRange = (Range) ranges.next();
2468                         // Writing MIME header.
2469                         writer.println();
2470                         writer.println("--" + mimeSeparation);
2471                         if (contentType != null)
2472                                 writer.println("Content-Type: " + contentType);
2473                         writer.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" + currentRange.length);
2474                         writer.println();
2475                         // Printing content
2476                         exception = copyRange(reader, writer, currentRange.start, currentRange.end);
2477                         reader.close();
2478                 }
2479                 writer.println();
2480                 writer.print("--" + mimeSeparation + "--");
2481                 // Rethrow any exception that has occurred
2482                 if (exception != null)
2483                         throw exception;
2484         }
2485
2486         /**
2487          * Copy the contents of the specified input stream to the specified output
2488          * stream, and ensure that both streams are closed before returning (even in
2489          * the face of an exception).
2490          *
2491          * @param istream The input stream to read from
2492          * @param ostream The output stream to write to
2493          * @param start Start of the range which will be copied
2494          * @param end End of the range which will be copied
2495          * @return Exception which occurred during processing
2496          */
2497         private IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) {
2498                 if (logger.isDebugEnabled())
2499                         logger.debug("Serving bytes:" + start + "-" + end);
2500                 try {
2501                         istream.skip(start);
2502                 } catch (IOException e) {
2503                         return e;
2504                 }
2505                 IOException exception = null;
2506                 long bytesToRead = end - start + 1;
2507                 byte buffer[] = new byte[input];
2508                 int len = buffer.length;
2509                 while (bytesToRead > 0 && len >= buffer.length) {
2510                         try {
2511                                 len = istream.read(buffer);
2512                                 if (bytesToRead >= len) {
2513                                         ostream.write(buffer, 0, len);
2514                                         bytesToRead -= len;
2515                                 } else {
2516                                         ostream.write(buffer, 0, (int) bytesToRead);
2517                                         bytesToRead = 0;
2518                                 }
2519                         } catch (IOException e) {
2520                                 exception = e;
2521                                 len = -1;
2522                         }
2523                         if (len < buffer.length)
2524                                 break;
2525                 }
2526                 return exception;
2527         }
2528
2529         /**
2530          * Copy the contents of the specified input stream to the specified output
2531          * stream, and ensure that both streams are closed before returning (even in
2532          * the face of an exception).
2533          *
2534          * @param reader The reader to read from
2535          * @param writer The writer to write to
2536          * @param start Start of the range which will be copied
2537          * @param end End of the range which will be copied
2538          * @return Exception which occurred during processing
2539          */
2540         private IOException copyRange(Reader reader, PrintWriter writer, long start, long end) {
2541                 try {
2542                         reader.skip(start);
2543                 } catch (IOException e) {
2544                         return e;
2545                 }
2546                 IOException exception = null;
2547                 long bytesToRead = end - start + 1;
2548                 char buffer[] = new char[input];
2549                 int len = buffer.length;
2550                 while (bytesToRead > 0 && len >= buffer.length) {
2551                         try {
2552                                 len = reader.read(buffer);
2553                                 if (bytesToRead >= len) {
2554                                         writer.write(buffer, 0, len);
2555                                         bytesToRead -= len;
2556                                 } else {
2557                                         writer.write(buffer, 0, (int) bytesToRead);
2558                                         bytesToRead = 0;
2559                                 }
2560                         } catch (IOException e) {
2561                                 exception = e;
2562                                 len = -1;
2563                         }
2564                         if (len < buffer.length)
2565                                 break;
2566                 }
2567                 return exception;
2568         }
2569
2570         /**
2571          * Copy the contents of the specified input stream to the specified output
2572          * stream, and ensure that both streams are closed before returning (even in
2573          * the face of an exception).
2574          *
2575          * @param file
2576          * @param ostream The output stream to write to
2577          * @param range Range the client wanted to retrieve
2578          * @param req the HTTP request
2579          * @param oldBody the old version of the file, if requested
2580          * @exception IOException if an input/output error occurs
2581          * @throws RpcException
2582          * @throws InsufficientPermissionsException
2583          * @throws ObjectNotFoundException
2584          */
2585         protected void copy(FileHeaderDTO file, ServletOutputStream ostream, Range range,
2586                                 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2587                                 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2588                 IOException exception = null;
2589                 User user = getUser(req);
2590                 InputStream resourceInputStream = oldBody == null ?
2591                                         getService().getFileContents(user.getId(), file.getId()) :
2592                                         getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2593                 InputStream istream = new BufferedInputStream(resourceInputStream, input);
2594                 exception = copyRange(istream, ostream, range.start, range.end);
2595                 // Clean up the input stream
2596                 istream.close();
2597                 // Rethrow any exception that has occurred
2598                 if (exception != null)
2599                         throw exception;
2600         }
2601
2602         /**
2603          * Copy the contents of the specified input stream to the specified output
2604          * stream, and ensure that both streams are closed before returning (even in
2605          * the face of an exception).
2606          *
2607          * @param file
2608          * @param writer The writer to write to
2609          * @param range Range the client wanted to retrieve
2610          * @param req the HTTP request
2611          * @param oldBody the old version of the file, if requested
2612          * @exception IOException if an input/output error occurs
2613          * @throws RpcException
2614          * @throws InsufficientPermissionsException
2615          * @throws ObjectNotFoundException
2616          */
2617         protected void copy(FileHeaderDTO file, PrintWriter writer, Range range,
2618                                 HttpServletRequest req, FileBodyDTO oldBody) throws IOException,
2619                                 ObjectNotFoundException, InsufficientPermissionsException, RpcException {
2620                 IOException exception = null;
2621                 User user = getUser(req);
2622                 InputStream resourceInputStream = oldBody == null ?
2623                                         getService().getFileContents(user.getId(), file.getId()) :
2624                                         getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2625                 Reader reader;
2626                 if (fileEncoding == null)
2627                         reader = new InputStreamReader(resourceInputStream);
2628                 else
2629                         reader = new InputStreamReader(resourceInputStream, fileEncoding);
2630
2631                 exception = copyRange(reader, writer, range.start, range.end);
2632                 // Clean up the input stream
2633                 reader.close();
2634                 // Rethrow any exception that has occurred
2635                 if (exception != null)
2636                         throw exception;
2637         }
2638
2639         /**
2640          * Copy the contents of the specified input stream to the specified output
2641          * stream, and ensure that both streams are closed before returning (even in
2642          * the face of an exception).
2643          *
2644          * @param file
2645          * @param ostream The output stream to write to
2646          * @param ranges Enumeration of the ranges the client wanted to retrieve
2647          * @param contentType Content type of the resource
2648          * @param req the HTTP request
2649          * @param oldBody the old version of the file, if requested
2650          * @exception IOException if an input/output error occurs
2651          * @throws RpcException
2652          * @throws InsufficientPermissionsException
2653          * @throws ObjectNotFoundException
2654          */
2655         protected void copy(FileHeaderDTO file, ServletOutputStream ostream,
2656                                 Iterator ranges, String contentType, HttpServletRequest req,
2657                                 FileBodyDTO oldBody) throws IOException, ObjectNotFoundException,
2658                                 InsufficientPermissionsException, RpcException {
2659                 IOException exception = null;
2660                 User user = getUser(req);
2661                 while (exception == null && ranges.hasNext()) {
2662                         InputStream resourceInputStream = oldBody == null ?
2663                                                 getService().getFileContents(user.getId(), file.getId()) :
2664                                                 getService().getFileContents(user.getId(), file.getId(), oldBody.getId());
2665                         InputStream istream = new BufferedInputStream(resourceInputStream, input);
2666                         Range currentRange = (Range) ranges.next();
2667                         // Writing MIME header.
2668                         ostream.println();
2669                         ostream.println("--" + mimeSeparation);
2670                         if (contentType != null)
2671                                 ostream.println("Content-Type: " + contentType);
2672                         ostream.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" + currentRange.length);
2673                         ostream.println();
2674
2675                         // Printing content
2676                         exception = copyRange(istream, ostream, currentRange.start, currentRange.end);
2677                         istream.close();
2678                 }
2679
2680                 ostream.println();
2681                 ostream.print("--" + mimeSeparation + "--");
2682                 // Rethrow any exception that has occurred
2683                 if (exception != null)
2684                         throw exception;
2685         }
2686
2687         /**
2688          * Return an InputStream to an HTML representation of the contents of this
2689          * directory.
2690          *
2691          * @param contextPath Context path to which our internal paths are relative
2692          * @param path the requested path to the resource
2693          * @param folder the specified directory
2694          * @param req the HTTP request
2695          * @return an input stream with the rendered contents
2696          * @throws IOException
2697          * @throws ServletException
2698          */
2699         private InputStream renderHtml(String contextPath, String path, FolderDTO folder, HttpServletRequest req) throws IOException, ServletException {
2700                 String name = folder.getName();
2701                 // Prepare a writer to a buffered area
2702                 ByteArrayOutputStream stream = new ByteArrayOutputStream();
2703                 OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
2704                 PrintWriter writer = new PrintWriter(osWriter);
2705                 StringBuffer sb = new StringBuffer();
2706                 // rewriteUrl(contextPath) is expensive. cache result for later reuse
2707                 String rewrittenContextPath = rewriteUrl(contextPath);
2708                 // Render the page header
2709                 sb.append("<html>\r\n");
2710                 sb.append("<head>\r\n");
2711                 sb.append("<title>");
2712                 sb.append("Index of " + name);
2713                 sb.append("</title>\r\n");
2714                 sb.append("<STYLE><!--");
2715                 sb.append(GSS_CSS);
2716                 sb.append("--></STYLE> ");
2717                 sb.append("</head>\r\n");
2718                 sb.append("<body>");
2719                 sb.append("<h1>");
2720                 sb.append("Index of " + name);
2721
2722                 // Render the link to our parent (if required)
2723                 String parentDirectory = path;
2724                 if (parentDirectory.endsWith("/"))
2725                         parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1);
2726                 int slash = parentDirectory.lastIndexOf('/');
2727                 if (slash >= 0) {
2728                         String parent = path.substring(0, slash);
2729                         sb.append(" - <a href=\"");
2730                         sb.append(rewrittenContextPath);
2731                         if (parent.equals(""))
2732                                 parent = "/";
2733                         sb.append(rewriteUrl(parent));
2734                         if (!parent.endsWith("/"))
2735                                 sb.append("/");
2736                         sb.append("\">");
2737                         sb.append("<b>");
2738                         sb.append("Up To " + parent);
2739                         sb.append("</b>");
2740                         sb.append("</a>");
2741                 }
2742
2743                 sb.append("</h1>");
2744                 sb.append("<HR size=\"1\" noshade=\"noshade\">");
2745
2746                 sb.append("<table width=\"100%\" cellspacing=\"0\"" + " cellpadding=\"5\" align=\"center\">\r\n");
2747
2748                 // Render the column headings
2749                 sb.append("<tr>\r\n");
2750                 sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
2751                 sb.append("Name");
2752                 sb.append("</strong></font></td>\r\n");
2753                 sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
2754                 sb.append("Size");
2755                 sb.append("</strong></font></td>\r\n");
2756                 sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
2757                 sb.append("Last modified");
2758                 sb.append("</strong></font></td>\r\n");
2759                 sb.append("</tr>");
2760                 // Render the directory entries within this directory
2761                 boolean shade = false;
2762                 Iterator iter = folder.getSubfolders().iterator();
2763                 while (iter.hasNext()) {
2764                         FolderDTO subf = (FolderDTO) iter.next();
2765                         String resourceName = subf.getName();
2766                         if (resourceName.equalsIgnoreCase("WEB-INF") || resourceName.equalsIgnoreCase("META-INF"))
2767                                 continue;
2768
2769                         sb.append("<tr");
2770                         if (shade)
2771                                 sb.append(" bgcolor=\"#eeeeee\"");
2772                         sb.append(">\r\n");
2773                         shade = !shade;
2774
2775                         sb.append("<td align=\"left\">&nbsp;&nbsp;\r\n");
2776                         sb.append("<a href=\"");
2777                         sb.append(rewrittenContextPath);
2778                         sb.append(rewriteUrl(path + resourceName));
2779                         sb.append("/");
2780                         sb.append("\"><tt>");
2781                         sb.append(RequestUtil.filter(resourceName));
2782                         sb.append("/");
2783                         sb.append("</tt></a></td>\r\n");
2784
2785                         sb.append("<td align=\"right\"><tt>");
2786                         sb.append("&nbsp;");
2787                         sb.append("</tt></td>\r\n");
2788
2789                         sb.append("<td align=\"right\"><tt>");
2790                         sb.append(getLastModifiedHttp(folder.getAuditInfo()));
2791                         sb.append("</tt></td>\r\n");
2792
2793                         sb.append("</tr>\r\n");
2794                 }
2795                 List<FileHeaderDTO> files;
2796                 try {
2797                         User user = getUser(req);
2798                         files = getService().getFiles(user.getId(), folder.getId(), true);
2799                 } catch (ObjectNotFoundException e) {
2800                         throw new ServletException(e.getMessage());
2801                 } catch (InsufficientPermissionsException e) {
2802                         throw new ServletException(e.getMessage());
2803                 } catch (RpcException e) {
2804                         throw new ServletException(e.getMessage());
2805                 }
2806                 for (FileHeaderDTO file : files) {
2807                         String resourceName = file.getName();
2808                         if (resourceName.equalsIgnoreCase("WEB-INF") || resourceName.equalsIgnoreCase("META-INF"))
2809                                 continue;
2810
2811                         sb.append("<tr");
2812                         if (shade)
2813                                 sb.append(" bgcolor=\"#eeeeee\"");
2814                         sb.append(">\r\n");
2815                         shade = !shade;
2816
2817                         sb.append("<td align=\"left\">&nbsp;&nbsp;\r\n");
2818                         sb.append("<a href=\"");
2819                         sb.append(rewrittenContextPath);
2820                         sb.append(rewriteUrl(path + resourceName));
2821                         sb.append("\"><tt>");
2822                         sb.append(RequestUtil.filter(resourceName));
2823                         sb.append("</tt></a></td>\r\n");
2824
2825                         sb.append("<td align=\"right\"><tt>");
2826                         sb.append(renderSize(file.getFileSize()));
2827                         sb.append("</tt></td>\r\n");
2828
2829                         sb.append("<td align=\"right\"><tt>");
2830                         sb.append(getLastModifiedHttp(file.getAuditInfo()));
2831                         sb.append("</tt></td>\r\n");
2832
2833                         sb.append("</tr>\r\n");
2834                 }
2835
2836                 // Render the page footer
2837                 sb.append("</table>\r\n");
2838
2839                 sb.append("<HR size=\"1\" noshade=\"noshade\">");
2840
2841                 sb.append("<h3>").append(getServletContext().getServerInfo()).append("</h3>");
2842                 sb.append("</body>\r\n");
2843                 sb.append("</html>\r\n");
2844
2845                 // Return an input stream to the underlying bytes
2846                 writer.write(sb.toString());
2847                 writer.flush();
2848                 return new ByteArrayInputStream(stream.toByteArray());
2849
2850         }
2851
2852         /**
2853          * Render the specified file size (in bytes).
2854          *
2855          * @param size File size (in bytes)
2856          * @return the size as a string
2857          */
2858         private String renderSize(long size) {
2859                 long leftSide = size / 1024;
2860                 long rightSide = size % 1024 / 103; // Makes 1 digit
2861                 if (leftSide == 0 && rightSide == 0 && size > 0)
2862                         rightSide = 1;
2863                 return "" + leftSide + "." + rightSide + " kb";
2864         }
2865
2866         /**
2867          * Copy a resource.
2868          *
2869          * @param req Servlet request
2870          * @param resp Servlet response
2871          * @return boolean true if the copy is successful
2872          * @throws IOException
2873          */
2874         private boolean copyResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
2875                 // Parsing destination header
2876                 String destinationPath = req.getHeader("Destination");
2877                 if (destinationPath == null) {
2878                         resp.sendError(WebdavStatus.SC_BAD_REQUEST);
2879                         return false;
2880                 }
2881
2882                 // Remove url encoding from destination
2883                 destinationPath = RequestUtil.URLDecode(destinationPath, "UTF8");
2884
2885                 int protocolIndex = destinationPath.indexOf("://");
2886                 if (protocolIndex >= 0) {
2887                         // if the Destination URL contains the protocol, we can safely
2888                         // trim everything upto the first "/" character after "://"
2889                         int firstSeparator = destinationPath.indexOf("/", protocolIndex + 4);
2890                         if (firstSeparator < 0)
2891                                 destinationPath = "/";
2892                         else
2893                                 destinationPath = destinationPath.substring(firstSeparator);
2894                 } else {
2895                         String hostName = req.getServerName();
2896                         if (hostName != null && destinationPath.startsWith(hostName))
2897                                 destinationPath = destinationPath.substring(hostName.length());
2898
2899                         int portIndex = destinationPath.indexOf(":");
2900                         if (portIndex >= 0)
2901                                 destinationPath = destinationPath.substring(portIndex);
2902
2903                         if (destinationPath.startsWith(":")) {
2904                                 int firstSeparator = destinationPath.indexOf("/");
2905                                 if (firstSeparator < 0)
2906                                         destinationPath = "/";
2907                                 else
2908                                         destinationPath = destinationPath.substring(firstSeparator);
2909                         }
2910                 }
2911
2912                 // Normalize destination path (remove '.' and '..')
2913                 destinationPath = RequestUtil.normalize(destinationPath);
2914
2915                 String contextPath = req.getContextPath();
2916                 if (contextPath != null && destinationPath.startsWith(contextPath))
2917                         destinationPath = destinationPath.substring(contextPath.length());
2918
2919                 String pathInfo = req.getPathInfo();
2920                 if (pathInfo != null) {
2921                         String servletPath = req.getServletPath();
2922                         if (servletPath != null && destinationPath.startsWith(servletPath))
2923                                 destinationPath = destinationPath.substring(servletPath.length());
2924                 }
2925
2926                 if (logger.isDebugEnabled())
2927                         logger.debug("Dest path :" + destinationPath);
2928
2929                 if (destinationPath.toUpperCase().startsWith("/WEB-INF") || destinationPath.toUpperCase().startsWith("/META-INF")) {
2930                         resp.sendError(WebdavStatus.SC_FORBIDDEN);
2931                         return false;
2932                 }
2933
2934                 String path = getRelativePath(req);
2935
2936                 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
2937                         resp.sendError(WebdavStatus.SC_FORBIDDEN);
2938                         return false;
2939                 }
2940
2941                 if (destinationPath.equals(path)) {
2942                         resp.sendError(WebdavStatus.SC_FORBIDDEN);
2943                         return false;
2944                 }
2945
2946                 // Parsing overwrite header
2947                 boolean overwrite = true;
2948                 String overwriteHeader = req.getHeader("Overwrite");
2949
2950                 if (overwriteHeader != null)
2951                         if (overwriteHeader.equalsIgnoreCase("T"))
2952                                 overwrite = true;
2953                         else
2954                                 overwrite = false;
2955
2956                 User user = getUser(req);
2957                 // Overwriting the destination
2958                 boolean exists = true;
2959                 try {
2960                         getService().getResourceAtPath(user.getId(), destinationPath, true);
2961                 } catch (ObjectNotFoundException e) {
2962                         exists = false;
2963                 } catch (RpcException e) {
2964                         resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
2965                         return false;
2966                 }
2967
2968                 if (overwrite) {
2969                         // Delete destination resource, if it exists
2970                         if (exists) {
2971                                 if (!deleteResource(destinationPath, req, resp, true))
2972                                         return false;
2973                         } else
2974                                 resp.setStatus(WebdavStatus.SC_CREATED);
2975                 } else // If the destination exists, then it's a conflict
2976                 if (exists) {
2977                         resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED);
2978                         return false;
2979                 } else
2980                         resp.setStatus(WebdavStatus.SC_CREATED);
2981
2982                 // Copying source to destination.
2983                 Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();
2984                 boolean result;
2985                 try {
2986                         result = copyResource(errorList, path, destinationPath, req);
2987                 } catch (RpcException e) {
2988                         resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR);
2989                         return false;
2990                 }
2991                 if (!result || !errorList.isEmpty()) {
2992                         sendReport(req, resp, errorList);
2993                         return false;
2994                 }
2995                 return true;
2996         }
2997
2998         /**
2999          * Copy a collection.
3000          *
3001          * @param errorList Hashtable containing the list of errors which occurred
3002          *            during the copy operation
3003          * @param source Path of the resource to be copied
3004          * @param theDest Destination path
3005          * @param req the HTTP request
3006          * @return boolean true if the copy is successful
3007          * @throws RpcException
3008          */
3009         private boolean copyResource(Hashtable<String, Integer> errorList, String source, String theDest, HttpServletRequest req) throws RpcException {
3010
3011                 String dest = theDest;
3012                 // Fix the destination path when copying collections.
3013                 if (source.endsWith("/") && !dest.endsWith("/"))
3014                         dest += "/";
3015
3016                 if (logger.isDebugEnabled())
3017                         logger.debug("Copy: " + source + " To: " + dest);
3018
3019                 final User user = getUser(req);
3020                 Object object = null;
3021                 try {
3022                         object = getService().getResourceAtPath(user.getId(), source, true);
3023                 } catch (ObjectNotFoundException e) {
3024                 }
3025
3026                 if (object instanceof FolderDTO) {
3027                         final FolderDTO folder = (FolderDTO) object;
3028                         try {
3029                                 final String des = dest;
3030                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3031                                         @Override
3032                                         public Void call() throws Exception {
3033                                                 getService().copyFolder(user.getId(), folder.getId(), des);
3034                                                 return null;
3035                                         }
3036                                 });
3037                         } catch (ObjectNotFoundException e) {
3038                                 errorList.put(dest, new Integer(WebdavStatus.SC_CONFLICT));
3039                                 return false;
3040                         } catch (DuplicateNameException e) {
3041                                 errorList.put(dest, new Integer(WebdavStatus.SC_CONFLICT));
3042                                 return false;
3043                         } catch (InsufficientPermissionsException e) {
3044                                 errorList.put(dest, new Integer(WebdavStatus.SC_FORBIDDEN));
3045                                 return false;
3046                         } catch (Exception e) {
3047                                 errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3048                                 return false;
3049                         }
3050
3051                         try {
3052                                 String newSource = source;
3053                                 if (!source.endsWith("/"))
3054                                         newSource += "/";
3055                                 String newDest = dest;
3056                                 if (!dest.endsWith("/"))
3057                                         newDest += "/";
3058                                 // Recursively copy the subfolders.
3059                                 Iterator iter = folder.getSubfolders().iterator();
3060                                 while (iter.hasNext()) {
3061                                         FolderDTO subf = (FolderDTO) iter.next();
3062                                         String resourceName = subf.getName();
3063                                         copyResource(errorList, newSource + resourceName, newDest + resourceName, req);
3064                                 }
3065                                 // Recursively copy the files.
3066                                 List<FileHeaderDTO> files;
3067                                 files = getService().getFiles(user.getId(), folder.getId(), true);
3068                                 for (FileHeaderDTO file : files) {
3069                                         String resourceName = file.getName();
3070                                         copyResource(errorList, newSource + resourceName, newDest + resourceName, req);
3071                                 }
3072                         } catch (RpcException e) {
3073                                 errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3074                                 return false;
3075                         } catch (ObjectNotFoundException e) {
3076                                 errorList.put(source, new Integer(WebdavStatus.SC_NOT_FOUND));
3077                                 return false;
3078                         } catch (InsufficientPermissionsException e) {
3079                                 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3080                                 return false;
3081                         }
3082
3083                 } else if (object instanceof FileHeaderDTO) {
3084                         final FileHeaderDTO file = (FileHeaderDTO) object;
3085                         try {
3086                                 final String des = dest;
3087                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3088                                         @Override
3089                                         public Void call() throws Exception {
3090                                                 getService().copyFile(user.getId(), file.getId(), des);
3091                                                 return null;
3092                                         }
3093                                 });
3094                         } catch (ObjectNotFoundException e) {
3095                                 errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3096                                 return false;
3097                         } catch (DuplicateNameException e) {
3098                                 errorList.put(source, new Integer(WebdavStatus.SC_CONFLICT));
3099                                 return false;
3100                         } catch (InsufficientPermissionsException e) {
3101                                 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3102                                 return false;
3103                         } catch (QuotaExceededException e) {
3104                                 errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN));
3105                                 return false;
3106                         } catch (GSSIOException e) {
3107                                 errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3108                                 return false;
3109                         } catch (Exception e) {
3110                                 errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3111                                 return false;
3112                         }
3113                 } else {
3114                         errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3115                         return false;
3116                 }
3117                 return true;
3118         }
3119
3120         /**
3121          * Delete a resource.
3122          *
3123          * @param req Servlet request
3124          * @param resp Servlet response
3125          * @return boolean true if the deletion is successful
3126          * @throws IOException
3127          */
3128         private boolean deleteResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
3129                 String path = getRelativePath(req);
3130                 return deleteResource(path, req, resp, true);
3131         }
3132
3133         /**
3134          * Delete a resource.
3135          *
3136          * @param path Path of the resource which is to be deleted
3137          * @param req Servlet request
3138          * @param resp Servlet response
3139          * @param setStatus Should the response status be set on successful
3140          *            completion
3141          * @return boolean true if the deletion is successful
3142          * @throws IOException
3143          */
3144         private boolean deleteResource(String path, HttpServletRequest req, HttpServletResponse resp, boolean setStatus) throws IOException {
3145                 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
3146                         resp.sendError(WebdavStatus.SC_FORBIDDEN);
3147                         return false;
3148                 }
3149                 String ifHeader = req.getHeader("If");
3150                 if (ifHeader == null)
3151                         ifHeader = "";
3152
3153                 String lockTokenHeader = req.getHeader("Lock-Token");
3154                 if (lockTokenHeader == null)
3155                         lockTokenHeader = "";
3156
3157                 if (isLocked(path, ifHeader + lockTokenHeader)) {
3158                         resp.sendError(WebdavStatus.SC_LOCKED);
3159                         return false;
3160                 }
3161
3162                 final User user = getUser(req);
3163                 boolean exists = true;
3164                 Object object = null;
3165                 try {
3166                         object = getService().getResourceAtPath(user.getId(), path, true);
3167                 } catch (ObjectNotFoundException e) {
3168                         exists = false;
3169                 } catch (RpcException e) {
3170                         resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
3171                         return false;
3172                 }
3173
3174                 if (!exists) {
3175                         resp.sendError(WebdavStatus.SC_NOT_FOUND);
3176                         return false;
3177                 }
3178
3179                 FolderDTO folder = null;
3180                 FileHeaderDTO file = null;
3181                 if (object instanceof FolderDTO)
3182                         folder = (FolderDTO) object;
3183                 else
3184                         file = (FileHeaderDTO) object;
3185
3186                 if (file != null)
3187                         try {
3188                                 final FileHeaderDTO f = file;
3189                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3190                                         @Override
3191                                         public Void call() throws Exception {
3192                                                 getService().deleteFile(user.getId(), f.getId());
3193                                                 return null;
3194                                         }
3195                                 });
3196                         } catch (InsufficientPermissionsException e) {
3197                                 resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED);
3198                                 return false;
3199                         } catch (ObjectNotFoundException e) {
3200                                 // Although we had already found the object, it was
3201                                 // probably deleted from another thread.
3202                                 resp.sendError(WebdavStatus.SC_NOT_FOUND);
3203                                 return false;
3204                         } catch (RpcException e) {
3205                                 resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
3206                                 return false;
3207                         } catch (Exception e) {
3208                                 resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR);
3209                                 return false;
3210                         }
3211                 else if (folder != null) {
3212                         Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();
3213                         deleteCollection(req, folder, path, errorList);
3214                         try {
3215                                 final FolderDTO f = folder;
3216                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3217                                         @Override
3218                                         public Void call() throws Exception {
3219                                                 getService().deleteFolder(user.getId(), f.getId());
3220                                                 return null;
3221                                         }
3222                                 });
3223                         } catch (InsufficientPermissionsException e) {
3224                                 errorList.put(path, new Integer(WebdavStatus.SC_METHOD_NOT_ALLOWED));
3225                         } catch (ObjectNotFoundException e) {
3226                                 errorList.put(path, new Integer(WebdavStatus.SC_NOT_FOUND));
3227                         } catch (RpcException e) {
3228                                 errorList.put(path, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3229                         } catch (Exception e) {
3230                                 errorList.put(path, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3231                         }
3232
3233                         if (!errorList.isEmpty()) {
3234                                 sendReport(req, resp, errorList);
3235                                 return false;
3236                         }
3237                 }
3238                 if (setStatus)
3239                         resp.setStatus(WebdavStatus.SC_NO_CONTENT);
3240                 return true;
3241         }
3242
3243         /**
3244          * Deletes a collection.
3245          *
3246          * @param req the HTTP request
3247          * @param folder the folder whose contents will be deleted
3248          * @param path Path to the collection to be deleted
3249          * @param errorList Contains the list of the errors which occurred
3250          */
3251         private void deleteCollection(HttpServletRequest req, FolderDTO folder, String path, Hashtable<String, Integer> errorList) {
3252
3253                 if (logger.isDebugEnabled())
3254                         logger.debug("Delete:" + path);
3255
3256                 if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) {
3257                         errorList.put(path, new Integer(WebdavStatus.SC_FORBIDDEN));
3258                         return;
3259                 }
3260
3261                 String ifHeader = req.getHeader("If");
3262                 if (ifHeader == null)
3263                         ifHeader = "";
3264
3265                 String lockTokenHeader = req.getHeader("Lock-Token");
3266                 if (lockTokenHeader == null)
3267                         lockTokenHeader = "";
3268
3269                 Iterator iter = folder.getSubfolders().iterator();
3270                 while (iter.hasNext()) {
3271                         FolderDTO subf = (FolderDTO) iter.next();
3272                         String childName = path;
3273                         if (!childName.equals("/"))
3274                                 childName += "/";
3275                         childName += subf.getName();
3276
3277                         if (isLocked(childName, ifHeader + lockTokenHeader))
3278                                 errorList.put(childName, new Integer(WebdavStatus.SC_LOCKED));
3279                         else
3280                                 try {
3281                                         final User user = getUser(req);
3282                                         Object object = getService().getResourceAtPath(user.getId(), childName, true);
3283                                         FolderDTO childFolder = null;
3284                                         FileHeaderDTO childFile = null;
3285                                         if (object instanceof FolderDTO)
3286                                                 childFolder = (FolderDTO) object;
3287                                         else
3288                                                 childFile = (FileHeaderDTO) object;
3289                                         if (childFolder != null) {
3290                                                 final FolderDTO cf = childFolder;
3291                                                 deleteCollection(req, childFolder, childName, errorList);
3292                                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3293                                                         @Override
3294                                                         public Void call() throws Exception {
3295                                                                 getService().deleteFolder(user.getId(), cf.getId());
3296                                                                 return null;
3297                                                         }
3298                                                 });
3299                                         } else if (childFile != null) {
3300                                                 final FileHeaderDTO cf = childFile;
3301                                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
3302                                                         @Override
3303                                                         public Void call() throws Exception {
3304                                                                 getService().deleteFile(user.getId(), cf.getId());
3305                                                                 return null;
3306                                                         }
3307                                                 });
3308                                         }
3309                                 } catch (ObjectNotFoundException e) {
3310                                         errorList.put(childName, new Integer(WebdavStatus.SC_NOT_FOUND));
3311                                 } catch (InsufficientPermissionsException e) {
3312                                         errorList.put(childName, new Integer(WebdavStatus.SC_FORBIDDEN));
3313                                 } catch (RpcException e) {
3314                                         errorList.put(childName, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3315                                 } catch (Exception e) {
3316                                         errorList.put(childName, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
3317                                 }
3318                 }
3319         }
3320
3321         /**
3322          * Send a multistatus element containing a complete error report to the
3323          * client.
3324          *
3325          * @param req Servlet request
3326          * @param resp Servlet response
3327          * @param errorList List of error to be displayed
3328          * @throws IOException
3329          */
3330         private void sendReport(HttpServletRequest req, HttpServletResponse resp, Hashtable errorList) throws IOException {
3331
3332                 resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
3333
3334                 String absoluteUri = req.getRequestURI();
3335                 String relativePath = getRelativePath(req);
3336
3337                 XMLWriter generatedXML = new XMLWriter();
3338                 generatedXML.writeXMLHeader();
3339
3340                 generatedXML.writeElement(null, "multistatus" + generateNamespaceDeclarations(), XMLWriter.OPENING);
3341
3342                 Enumeration pathList = errorList.keys();
3343                 while (pathList.hasMoreElements()) {
3344
3345                         String errorPath = (String) pathList.nextElement();
3346                         int errorCode = ((Integer) errorList.get(errorPath)).intValue();
3347
3348                         generatedXML.writeElement(null, "response", XMLWriter.OPENING);
3349
3350                         generatedXML.writeElement(null, "href", XMLWriter.OPENING);
3351                         String toAppend = errorPath.substring(relativePath.length());
3352                         if (!toAppend.startsWith("/"))
3353                                 toAppend = "/" + toAppend;
3354                         generatedXML.writeText(absoluteUri + toAppend);
3355                         generatedXML.writeElement(null, "href", XMLWriter.CLOSING);
3356                         generatedXML.writeElement(null, "status", XMLWriter.OPENING);
3357                         generatedXML.writeText("HTTP/1.1 " + errorCode + " " + WebdavStatus.getStatusText(errorCode));
3358                         generatedXML.writeElement(null, "status", XMLWriter.CLOSING);
3359
3360                         generatedXML.writeElement(null, "response", XMLWriter.CLOSING);
3361
3362                 }
3363
3364                 generatedXML.writeElement(null, "multistatus", XMLWriter.CLOSING);
3365
3366                 Writer writer = resp.getWriter();
3367                 writer.write(generatedXML.toString());
3368                 writer.close();
3369
3370         }
3371
3372         // --------------------------------------------- WebdavResolver Inner Class
3373         /**
3374          * Work around for XML parsers that don't fully respect
3375          * {@link DocumentBuilderFactory#setExpandEntityReferences(boolean)}.
3376          * External references are filtered out for security reasons. See
3377          * CVE-2007-5461.
3378          */
3379         private class WebdavResolver implements EntityResolver {
3380
3381                 /**
3382                  * A private copy of the servlet context.
3383                  */
3384                 private ServletContext context;
3385
3386                 /**
3387                  * Construct the resolver by passing the servlet context.
3388                  *
3389                  * @param theContext the servlet context
3390                  */
3391                 public WebdavResolver(ServletContext theContext) {
3392                         context = theContext;
3393                 }
3394
3395                 public InputSource resolveEntity(String publicId, String systemId) {
3396                         context.log("The request included a reference to an external entity with PublicID " + publicId + " and SystemID " + systemId + " which was ignored");
3397                         return new InputSource(new StringReader("Ignored external entity"));
3398                 }
3399         }
3400
3401         /**
3402          * Returns the user making the request. This is the user whose credentials
3403          * were supplied in the authorization header.
3404          *
3405          * @param req the HTTP request
3406          * @return the user making the request
3407          */
3408         protected User getUser(HttpServletRequest req) {
3409                 return (User) req.getAttribute(USER_ATTRIBUTE);
3410         }
3411
3412         /**
3413          * Retrieves the user who owns the requested namespace, as specified in the
3414          * REST API.
3415          *
3416          * @param req the HTTP request
3417          * @return the owner of the namespace
3418          */
3419         protected User getOwner(HttpServletRequest req) {
3420                 return (User) req.getAttribute(OWNER_ATTRIBUTE);
3421         }
3422
3423 }