Redirect to login for browser requests without a cookie present and also for requests...
[pithos] / src / gr / ebs / gss / server / rest / FilesHandler.java
1 /*
2  * Copyright 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.rest;
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.Login;
29 import gr.ebs.gss.server.domain.FileUploadStatus;
30 import gr.ebs.gss.server.domain.User;
31 import gr.ebs.gss.server.domain.dto.FileBodyDTO;
32 import gr.ebs.gss.server.domain.dto.FileHeaderDTO;
33 import gr.ebs.gss.server.domain.dto.FolderDTO;
34 import gr.ebs.gss.server.domain.dto.GroupDTO;
35 import gr.ebs.gss.server.domain.dto.PermissionDTO;
36 import gr.ebs.gss.server.ejb.ExternalAPI;
37 import gr.ebs.gss.server.ejb.TransactionHelper;
38 import gr.ebs.gss.server.webdav.Range;
39
40 import java.io.BufferedReader;
41 import java.io.ByteArrayInputStream;
42 import java.io.ByteArrayOutputStream;
43 import java.io.File;
44 import java.io.FileInputStream;
45 import java.io.FileNotFoundException;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.InputStreamReader;
49 import java.io.OutputStreamWriter;
50 import java.io.PrintWriter;
51 import java.io.UnsupportedEncodingException;
52 import java.net.URI;
53 import java.net.URISyntaxException;
54 import java.net.URLDecoder;
55 import java.net.URLEncoder;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collection;
59 import java.util.Date;
60 import java.util.HashSet;
61 import java.util.List;
62 import java.util.Set;
63 import java.util.StringTokenizer;
64 import java.util.concurrent.Callable;
65
66 import javax.servlet.ServletContext;
67 import javax.servlet.ServletException;
68 import javax.servlet.ServletOutputStream;
69 import javax.servlet.http.Cookie;
70 import javax.servlet.http.HttpServletRequest;
71 import javax.servlet.http.HttpServletResponse;
72
73 import org.apache.commons.codec.binary.Base64;
74 import org.apache.commons.fileupload.FileItemIterator;
75 import org.apache.commons.fileupload.FileItemStream;
76 import org.apache.commons.fileupload.FileUploadException;
77 import org.apache.commons.fileupload.ProgressListener;
78 import org.apache.commons.fileupload.servlet.ServletFileUpload;
79 import org.apache.commons.fileupload.util.Streams;
80 import org.apache.commons.httpclient.util.DateParseException;
81 import org.apache.commons.httpclient.util.DateUtil;
82 import org.apache.commons.logging.Log;
83 import org.apache.commons.logging.LogFactory;
84 import org.json.JSONArray;
85 import org.json.JSONException;
86 import org.json.JSONObject;
87
88
89 /**
90  * A class that handles operations on the 'files' namespace.
91  *
92  * @author past
93  */
94 public class FilesHandler extends RequestHandler {
95         /**
96          * The request parameter name for fetching a different version.
97          */
98         private static final String VERSION_PARAM = "version";
99
100         /**
101          * The request attribute containing the owner of the destination URI
102          * in a copy or move request.
103          */
104         private static final String DESTINATION_OWNER_ATTRIBUTE = "destOwner";
105
106         private static final int TRACK_PROGRESS_PERCENT = 5;
107
108         /**
109          * The form parameter name that contains the signature in a browser POST upload.
110          */
111         private static final String AUTHORIZATION_PARAMETER = "Authorization";
112
113         /**
114          * The form parameter name that contains the date in a browser POST upload.
115          */
116         private static final String DATE_PARAMETER = "Date";
117
118         /**
119          * The request parameter name for making an upload progress request.
120          */
121         private static final String PROGRESS_PARAMETER = "progress";
122
123         /**
124          * The request parameter name for restoring a previous version of a file.
125          */
126         private static final String RESTORE_VERSION_PARAMETER = "restoreVersion";
127
128         /**
129          * The logger.
130          */
131         private static Log logger = LogFactory.getLog(FilesHandler.class);
132
133         /**
134          * The servlet context provided by the call site.
135          */
136         private ServletContext context;
137
138         /**
139          * @param servletContext
140          */
141         public FilesHandler(ServletContext servletContext) {
142                 context = servletContext;
143         }
144
145         private void updateAccounting(final User user, final Date date, final long bandwidthDiff) {
146                 try {
147                         new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
148                                 @Override
149                                 public Void call() throws Exception {
150                                         getService().updateAccounting(user, date, bandwidthDiff);
151                                         return null;
152                                 }
153                         });
154                 } catch (RuntimeException e) {
155                         throw e;
156                 } catch (Exception e) {
157                         // updateAccounting() doesn't throw any checked exceptions
158                         assert false;
159                 }
160         }
161
162         /**
163      * Serve the specified resource, optionally including the data content.
164      *
165      * @param req The servlet request we are processing
166      * @param resp The servlet response we are creating
167      * @param content Should the content be included?
168      *
169      * @exception IOException if an input/output error occurs
170      * @exception ServletException if a servlet-specified error occurs
171      * @throws RpcException
172      * @throws InsufficientPermissionsException
173      * @throws ObjectNotFoundException
174      */
175         @Override
176         protected void serveResource(HttpServletRequest req, HttpServletResponse resp, boolean content)
177                 throws IOException, ServletException {
178                 boolean authDeferred = getAuthDeferred(req);
179         String path = getInnerPath(req, PATH_FILES);
180                 if (path.equals(""))
181                         path = "/";
182                 try {
183                         path = URLDecoder.decode(path, "UTF-8");
184                 } catch (IllegalArgumentException e) {
185                 resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
186                         return;
187                 }
188         String progress = req.getParameter(PROGRESS_PARAMETER);
189
190         if (logger.isDebugEnabled())
191                         if (content)
192                         logger.debug("Serving resource '" +     path + "' headers and data");
193                 else
194                         logger.debug("Serving resource '" +     path + "' headers only");
195
196         User user = getUser(req);
197         User owner = getOwner(req);
198         if (user == null) user = owner;
199         boolean exists = true;
200         Object resource = null;
201         FileHeaderDTO file = null;
202         FolderDTO folder = null;
203         try {
204                 resource = getService().getResourceAtPath(owner.getId(), path, false);
205         } catch (ObjectNotFoundException e) {
206             exists = false;
207         } catch (RpcException e) {
208                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
209                         return;
210                 }
211
212         if (!exists) {
213                         if (authDeferred) {
214                                 // We do not want to leak information if the request
215                                 // was not authenticated.
216                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
217                                 return;
218                         }
219                 // A request for upload progress.
220                 if (progress != null && content) {
221                         serveProgress(req, resp, progress, user, null);
222                                 return;
223                 }
224
225                 resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
226                 return;
227         }
228
229         if (resource instanceof FolderDTO)
230                 folder = (FolderDTO) resource;
231         else
232                 file = (FileHeaderDTO) resource;
233
234         // Now it's time to perform the deferred authentication check.
235                 // Since regular signature checking was already performed,
236                 // we need to check the read-all flag or the signature-in-parameters.
237                 if (authDeferred)
238                         if (file != null && !file.isReadForAll() && content) {
239                                 // Check for GET with the signature in the request parameters.
240                                 String auth = req.getParameter(AUTHORIZATION_PARAMETER);
241                                 String dateParam = req.getParameter(DATE_PARAMETER);
242                                 if (auth == null || dateParam == null) {
243                                         // Check for a valid authentication cookie.
244                                         if (req.getCookies() != null) {
245                                                 boolean found = false;
246                                                 for (Cookie cookie : req.getCookies())
247                                                         if (Login.AUTH_COOKIE.equals(cookie.getName())) {
248                                                                 String cookieauth = cookie.getValue();
249                                                                 int sepIndex = cookieauth.indexOf(Login.COOKIE_SEPARATOR);
250                                                                 if (sepIndex == -1) {
251                                                                         handleAuthFailure(req, resp);
252                                                                         return;
253                                                                 }
254                                                                 String username = URLDecoder.decode(cookieauth.substring(0, sepIndex), "US-ASCII");
255                                                                 user = null;
256                                                                 try {
257                                                                         user = getService().findUser(username);
258                                                                 } catch (RpcException e) {
259                                                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
260                                                                         return;
261                                                                 }
262                                                                 if (user == null) {
263                                                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
264                                                                 return;
265                                                         }
266                                                                 req.setAttribute(USER_ATTRIBUTE, user);
267                                                                 String token = cookieauth.substring(sepIndex + 1);
268                                                                 if (user.getAuthToken() == null) {
269                                                                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
270                                                                         return;
271                                                                 }
272                                                                 if (!Arrays.equals(user.getAuthToken(), Base64.decodeBase64(token))) {
273                                                                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
274                                                                         return;
275                                                                 }
276                                                                 found = true;
277                                                                 break;
278                                                         }
279                                                 if (!found) {
280                                                         handleAuthFailure(req, resp);
281                                                         return;
282                                                 }
283                                         } else {
284                                                 handleAuthFailure(req, resp);
285                                                 return;
286                                         }
287                                 } else {
288                                 long timestamp;
289                                         try {
290                                                 timestamp = DateUtil.parseDate(dateParam).getTime();
291                                         } catch (DateParseException e) {
292                                         resp.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
293                                         return;
294                                         }
295                                 if (!isTimeValid(timestamp)) {
296                                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
297                                         return;
298                                 }
299
300                                         // Fetch the Authorization parameter and find the user specified in it.
301                                         String[] authParts = auth.split(" ");
302                                         if (authParts.length != 2) {
303                                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
304                                         return;
305                                 }
306                                         String username = authParts[0];
307                                         String signature = authParts[1];
308                                         user = null;
309                                         try {
310                                                 user = getService().findUser(username);
311                                         } catch (RpcException e) {
312                                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
313                                                 return;
314                                         }
315                                         if (user == null) {
316                                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
317                                         return;
318                                 }
319                                         req.setAttribute(USER_ATTRIBUTE, user);
320
321                                         // Remove the servlet path from the request URI.
322                                         String p = req.getRequestURI();
323                                         String servletPath = req.getContextPath() + req.getServletPath();
324                                         p = p.substring(servletPath.length());
325                                         // Validate the signature in the Authorization parameter.
326                                         String data = req.getMethod() + dateParam + p;
327                                         if (!isSignatureValid(signature, user, data)) {
328                                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
329                                         return;
330                                 }
331                                 }
332                         } else if (file != null && !file.isReadForAll() || file == null) {
333                                 // Check for a read-for-all file request.
334                                 resp.sendError(HttpServletResponse.SC_FORBIDDEN);
335                                 return;
336                         }
337
338         // If the resource is not a collection, and the resource path
339         // ends with "/" or "\", return NOT FOUND.
340         if (folder == null)
341                         if (path.endsWith("/") || path.endsWith("\\")) {
342                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI());
343                         return;
344                 }
345
346         // Workaround for IE's broken caching behavior.
347         if (folder != null)
348                 resp.setHeader("Expires", "-1");
349
350         // A request for upload progress.
351         if (progress != null && content) {
352                 if (file == null) {
353                         resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
354                         return;
355                 }
356                 serveProgress(req, resp, progress, user, file);
357                         return;
358         }
359
360                 // Fetch the version to retrieve, if specified.
361                 String verStr = req.getParameter(VERSION_PARAM);
362                 int version = 0;
363                 FileBodyDTO oldBody = null;
364                 if (verStr != null && file != null)
365                         try {
366                                 version = Integer.valueOf(verStr);
367                         } catch (NumberFormatException e) {
368                                 resp.sendError(HttpServletResponse.SC_BAD_REQUEST, req.getRequestURI());
369                         return;
370                         }
371                 if (version > 0)
372                         try {
373                                 oldBody = getService().getFileVersion(user.getId(), file.getId(), version);
374                         } catch (RpcException e) {
375                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
376                                 return;
377                         } catch (ObjectNotFoundException e) {
378                         resp.sendError(HttpServletResponse.SC_NOT_FOUND);
379                         return;
380                         } catch (InsufficientPermissionsException e) {
381                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
382                         return;
383                         }
384
385         // Check if the conditions specified in the optional If headers are
386         // satisfied. Doing this for folders would require recursive checking
387         // for all of their children, which in turn would defy the purpose of
388         // the optimization.
389         if (folder == null){
390                         // Checking If headers.
391                 if (!checkIfHeaders(req, resp, file, oldBody))
392                                 return;
393         }
394         else if(!checkIfModifiedSince(req, resp, folder))
395                 return;
396
397         // Find content type.
398         String contentType = null;
399         if (file != null) {
400                 contentType = version>0 ? oldBody.getMimeType() : file.getMimeType();
401                 if (contentType == null) {
402                         contentType = context.getMimeType(file.getName());
403                         file.setMimeType(contentType);
404                 }
405         } else
406                         contentType = "application/json;charset=UTF-8";
407
408         ArrayList ranges = null;
409         long contentLength = -1L;
410
411         if (file != null) {
412                 // Parse range specifier.
413                 ranges = parseRange(req, resp, file, oldBody);
414                 // ETag header
415                 resp.setHeader("ETag", getETag(file, oldBody));
416                 // Last-Modified header.
417                 String lastModified = oldBody == null ?
418                                         getLastModifiedHttp(file.getAuditInfo()) :
419                                         getLastModifiedHttp(oldBody.getAuditInfo());
420                 resp.setHeader("Last-Modified", lastModified);
421                 // X-GSS-Metadata header.
422                 try {
423                                 resp.setHeader("X-GSS-Metadata", renderJson(user, file, oldBody));
424                         } catch (InsufficientPermissionsException e) {
425                                 resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
426                         return;
427                 }
428                 // Get content length.
429                 contentLength = version>0 ? oldBody.getFileSize() : file.getFileSize();
430                 // Special case for zero length files, which would cause a
431                 // (silent) ISE when setting the output buffer size.
432                 if (contentLength == 0L)
433                                 content = false;
434         } else
435                 // Set the folder X-GSS-Metadata header.
436                 try {
437                                 resp.setHeader("X-GSS-Metadata", renderJsonMetadata(user, folder));
438                         } catch (InsufficientPermissionsException e) {
439                                 resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
440                         return;
441                 }
442
443         ServletOutputStream ostream = null;
444         PrintWriter writer = null;
445
446         if (content)
447                         try {
448                         ostream = resp.getOutputStream();
449                 } catch (IllegalStateException e) {
450                         // If it fails, we try to get a Writer instead if we're
451                         // trying to serve a text file
452                         if ( contentType == null
453                                                 || contentType.startsWith("text")
454                                                 || contentType.endsWith("xml") )
455                                         writer = resp.getWriter();
456                                 else
457                                         throw e;
458                 }
459
460         if (folder != null
461                                 || (ranges == null || ranges.isEmpty())
462                                                         && req.getHeader("Range") == null
463                                                         || ranges == FULL) {
464                 // Set the appropriate output headers
465                 if (contentType != null) {
466                         if (logger.isDebugEnabled())
467                                 logger.debug("contentType='" + contentType + "'");
468                         resp.setContentType(contentType);
469                 }
470                 if (file != null && contentLength >= 0) {
471                         if (logger.isDebugEnabled())
472                                 logger.debug("contentLength=" + contentLength);
473                         if (contentLength < Integer.MAX_VALUE)
474                                         resp.setContentLength((int) contentLength);
475                                 else
476                                         // Set the content-length as String to be able to use a long
477                                 resp.setHeader("content-length", "" + contentLength);
478                 }
479
480                 InputStream renderResult = null;
481                 if (folder != null)
482                                 if (content)
483                                         // Serve the directory browser
484                                 try {
485                                                 renderResult = renderJson(user, folder);
486                                         } catch (InsufficientPermissionsException e) {
487                                                 resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
488                                         return;
489                                         }
490                 // Copy the input stream to our output stream (if requested)
491                 if (content) {
492                         try {
493                                 resp.setBufferSize(output);
494                         } catch (IllegalStateException e) {
495                                 // Silent catch
496                         }
497                         try {
498                                 if(file != null)
499                                                 if (needsContentDisposition(req))
500                                                 resp.setHeader("Content-Disposition","attachment; filename*=UTF-8''"+getDispositionFilename(file));
501                                         else
502                                                 resp.setHeader("Content-Disposition","inline; filename*=UTF-8''"+getDispositionFilename(file));
503                                 if (ostream != null)
504                                                 copy(file, renderResult, ostream, req, oldBody);
505                                         else
506                                                 copy(file, renderResult, writer, req, oldBody);
507                                 if (file!=null) updateAccounting(owner, new Date(), contentLength);
508                         } catch (ObjectNotFoundException e) {
509                                 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
510                                 return;
511                         } catch (InsufficientPermissionsException e) {
512                                 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
513                                 return;
514                         } catch (RpcException e) {
515                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
516                                 return;
517                         }
518                 }
519         } else {
520                 if (ranges == null || ranges.isEmpty())
521                         return;
522                 // Partial content response.
523                 resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
524
525                 if (ranges.size() == 1) {
526                         Range range = (Range) ranges.get(0);
527                         resp.addHeader("Content-Range", "bytes "
528                                                 + range.start
529                                                 + "-" + range.end + "/"
530                                                 + range.length);
531                         long length = range.end - range.start + 1;
532                         if (length < Integer.MAX_VALUE)
533                                         resp.setContentLength((int) length);
534                                 else
535                                         // Set the content-length as String to be able to use a long
536                                 resp.setHeader("content-length", "" + length);
537
538                         if (contentType != null) {
539                                 if (logger.isDebugEnabled())
540                                         logger.debug("contentType='" + contentType + "'");
541                                 resp.setContentType(contentType);
542                         }
543
544                         if (content) {
545                                 try {
546                                         resp.setBufferSize(output);
547                                 } catch (IllegalStateException e) {
548                                         // Silent catch
549                                 }
550                                 try {
551                                         if (ostream != null)
552                                                         copy(file, ostream, range, req, oldBody);
553                                                 else
554                                                         copy(file, writer, range, req, oldBody);
555                                         updateAccounting(owner, new Date(), contentLength);
556                         } catch (ObjectNotFoundException e) {
557                                 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
558                                 return;
559                         } catch (InsufficientPermissionsException e) {
560                                 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
561                                 return;
562                         } catch (RpcException e) {
563                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
564                                 return;
565                         }
566                         }
567                 } else {
568                         resp.setContentType("multipart/byteranges; boundary=" + mimeSeparation);
569                         if (content) {
570                                 try {
571                                         resp.setBufferSize(output);
572                                 } catch (IllegalStateException e) {
573                                         // Silent catch
574                                 }
575                                 try {
576                                         if (ostream != null)
577                                                         copy(file, ostream, ranges.iterator(), contentType, req, oldBody);
578                                                 else
579                                                         copy(file, writer, ranges.iterator(), contentType, req, oldBody);
580                                         updateAccounting(owner, new Date(), contentLength);
581                         } catch (ObjectNotFoundException e) {
582                                 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
583                                 return;
584                         } catch (InsufficientPermissionsException e) {
585                                 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
586                                 return;
587                         } catch (RpcException e) {
588                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
589                                 return;
590                         }
591                         }
592                 }
593         }
594     }
595
596         /**
597          * Handles an authentication failure. If no Authorization or Date request
598          * parameters and no Authorization, Date or X-GSS-Date headers were present,
599          * this is a browser request, so redirect to login and then let the user get
600          * back to the file. Otherwise it's a bogus client request and Forbidden is
601          * returned.
602          */
603         private void handleAuthFailure(HttpServletRequest req, HttpServletResponse resp) throws IOException {
604                 if (req.getParameter(AUTHORIZATION_PARAMETER) == null &&
605                                 req.getParameter(DATE_PARAMETER) == null &&
606                                 req.getHeader(AUTHORIZATION_HEADER) == null &&
607                                 req.getDateHeader(DATE_HEADER) == -1 &&
608                                 req.getDateHeader(GSS_DATE_HEADER) == -1)
609                         resp.sendRedirect(getConfiguration().getString("loginUrl") +
610                                         "?next=" + req.getRequestURL().toString());
611                 else
612                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
613         }
614
615         /**
616          * Return the filename of the specified file properly formatted for
617          * including in the Content-Disposition header.
618          */
619         private String getDispositionFilename(FileHeaderDTO file) throws UnsupportedEncodingException {
620                 return URLEncoder.encode(file.getName(),"UTF-8").replaceAll("\\+", "%20");
621         }
622
623         /**
624          * Determines whether the user agent needs the Content-Disposition
625          * header to be set, in order to properly download a file.
626          *
627          * @param req the HTTP request
628          * @return true if the Content-Disposition HTTP header must be set
629          */
630         private boolean needsContentDisposition(HttpServletRequest req) {
631                 /*String agent = req.getHeader("user-agent");
632                 if (agent != null && agent.contains("MSIE"))
633                         return true;*/
634                 String dl = req.getParameter("dl");
635                 if ("1".equals(dl))
636                         return true;
637                 return false;
638         }
639
640         /**
641          * Sends a progress update on the amount of bytes received until now for
642          * a file that the current user is currently uploading.
643          *
644          * @param req the HTTP request
645          * @param resp the HTTP response
646          * @param parameter the value for the progress request parameter
647          * @param user the current user
648          * @param file the file being uploaded, or null if the request is about a new file
649          * @throws IOException if an I/O error occurs
650          */
651         private void serveProgress(HttpServletRequest req, HttpServletResponse resp,
652                                 String parameter, User user, FileHeaderDTO file)        throws IOException {
653                 String filename = file == null ? parameter : file.getName();
654                 try {
655                         FileUploadStatus status = getService().getFileUploadStatus(user.getId(), filename);
656                         if (status == null) {
657                                 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
658                                 return;
659                         }
660                         JSONObject json = new JSONObject();
661                         json.put("bytesUploaded", status.getBytesUploaded()).
662                                 put("bytesTotal", status.getFileSize());
663                         sendJson(req, resp, json.toString());
664
665                         // Workaround for IE's broken caching behavior.
666                 resp.setHeader("Expires", "-1");
667                         return;
668                 } catch (RpcException e) {
669                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
670                         return;
671                 } catch (JSONException e) {
672                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
673                         return;
674                 }
675         }
676
677         /**
678          * Server a POST request to create/modify a file or folder.
679          *
680          * @param req the HTTP request
681          * @param resp the HTTP response
682      * @exception IOException if an input/output error occurs
683          */
684         void postResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
685                 boolean authDeferred = getAuthDeferred(req);
686         if (!authDeferred && req.getParameterMap().size() > 1) {
687                 resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
688                 return;
689         }
690         String path = getInnerPath(req, PATH_FILES);
691         path = path.endsWith("/")? path: path + '/';
692                 try {
693                 path = URLDecoder.decode(path, "UTF-8");
694                 } catch (IllegalArgumentException e) {
695                         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
696                         return;
697                 }
698         // We only defer authenticating multipart POST requests.
699         if (authDeferred) {
700                         if (!ServletFileUpload.isMultipartContent(req)) {
701                         resp.sendError(HttpServletResponse.SC_FORBIDDEN);
702                         return;
703                 }
704                         handleMultipart(req, resp, path);
705                         return;
706                 }
707
708         String newName = req.getParameter(NEW_FOLDER_PARAMETER);
709         boolean hasUpdateParam = req.getParameterMap().containsKey(RESOURCE_UPDATE_PARAMETER);
710         boolean hasTrashParam = req.getParameterMap().containsKey(RESOURCE_TRASH_PARAMETER);
711         boolean hasRestoreParam = req.getParameterMap().containsKey(RESOURCE_RESTORE_PARAMETER);
712         String copyTo = req.getParameter(RESOURCE_COPY_PARAMETER);
713         String moveTo = req.getParameter(RESOURCE_MOVE_PARAMETER);
714         String restoreVersion = req.getParameter(RESTORE_VERSION_PARAMETER);
715
716         if (newName != null)
717                         createFolder(req, resp, path, newName);
718         else if (hasUpdateParam)
719                         updateResource(req, resp, path);
720                 else if (hasTrashParam)
721                         trashResource(req, resp, path);
722                 else if (hasRestoreParam)
723                         restoreResource(req, resp, path);
724                 else if (copyTo != null)
725                         copyResource(req, resp, path, copyTo);
726                 else if (moveTo != null)
727                         moveResource(req, resp, path, moveTo);
728                 else if (restoreVersion != null)
729                         restoreVersion(req, resp, path, restoreVersion);
730                 else
731                         // IE with Gears uses POST for multiple uploads.
732                         putResource(req, resp);
733         }
734
735         /**
736          * Restores a previous version for a file.
737          *
738          * @param req the HTTP request
739          * @param resp the HTTP response
740          * @param path the resource path
741          * @param version the version number to restore
742          * @throws IOException if an I/O error occurs
743          */
744         private void restoreVersion(HttpServletRequest req, HttpServletResponse resp, String path, String version) throws IOException {
745                 final User user = getUser(req);
746                 User owner = getOwner(req);
747                 Object resource = null;
748                 try {
749                         resource = getService().getResourceAtPath(owner.getId(), path, true);
750                 } catch (ObjectNotFoundException e) {
751                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
752                         return;
753                 } catch (RpcException e) {
754                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
755                         return;
756                 }
757                 if (resource instanceof FolderDTO) {
758                         resp.sendError(HttpServletResponse.SC_CONFLICT);
759                         return;
760                 }
761
762                 try {
763                         final FileHeaderDTO file = (FileHeaderDTO) resource;
764                         final int oldVersion = Integer.parseInt(version);
765
766                         new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
767                                 @Override
768                                 public Void call() throws Exception {
769                                         getService().restoreVersion(user.getId(), file.getId(), oldVersion);
770                                         return null;
771                                 }
772                         });
773                 } catch (InsufficientPermissionsException e) {
774                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
775                 } catch (ObjectNotFoundException e) {
776                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
777                 } catch (RpcException e) {
778                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
779                 } catch (GSSIOException e) {
780                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
781                 } catch (QuotaExceededException e) {
782                         resp.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, e.getMessage());
783                 } catch (NumberFormatException e) {
784                         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
785                 } catch (Exception e) {
786                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
787                 }
788         }
789
790         /**
791          * A method for handling multipart POST requests for uploading
792          * files from browser-based JavaScript clients.
793          *
794          * @param request the HTTP request
795          * @param response the HTTP response
796          * @param path the resource path
797          * @throws IOException in case an error occurs writing to the
798          *              response stream
799          */
800         private void handleMultipart(HttpServletRequest request, HttpServletResponse response, String path) throws IOException {
801         if (logger.isDebugEnabled())
802                         logger.debug("Multipart POST for resource: " + path);
803
804         User owner = getOwner(request);
805         boolean exists = true;
806         Object resource = null;
807         FileHeaderDTO file = null;
808         try {
809                 resource = getService().getResourceAtPath(owner.getId(), path, false);
810         } catch (ObjectNotFoundException e) {
811             exists = false;
812         } catch (RpcException e) {
813                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
814                         return;
815                 }
816
817         if (exists)
818                         if (resource instanceof FileHeaderDTO) {
819                         file = (FileHeaderDTO) resource;
820                         if (file.isDeleted()) {
821                                 response.sendError(HttpServletResponse.SC_CONFLICT, file.getName() + " is in the trash");
822                         return;
823                         }
824                         } else {
825                         response.sendError(HttpServletResponse.SC_CONFLICT, path + " is a folder");
826                         return;
827                 }
828
829         Object parent;
830         String parentPath = null;
831                 try {
832                         parentPath = getParentPath(path);
833                         parent = getService().getResourceAtPath(owner.getId(), parentPath, true);
834                 } catch (ObjectNotFoundException e) {
835                 response.sendError(HttpServletResponse.SC_NOT_FOUND, parentPath);
836                 return;
837                 } catch (RpcException e) {
838                 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
839                         return;
840                 }
841         if (!(parent instanceof FolderDTO)) {
842                 response.sendError(HttpServletResponse.SC_CONFLICT);
843                 return;
844         }
845         final FolderDTO folder = (FolderDTO) parent;
846         final String fileName = getLastElement(path);
847
848                 FileItemIterator iter;
849                 File uploadedFile = null;
850                 try {
851                         // Create a new file upload handler.
852                         ServletFileUpload upload = new ServletFileUpload();
853                         StatusProgressListener progressListener = new StatusProgressListener(getService());
854                         upload.setProgressListener(progressListener);
855                         iter = upload.getItemIterator(request);
856                         String dateParam = null;
857                         String auth = null;
858                         while (iter.hasNext()) {
859                                 FileItemStream item = iter.next();
860                                 String name = item.getFieldName();
861                                 InputStream stream = item.openStream();
862                                 if (item.isFormField()) {
863                                         final String value = Streams.asString(stream);
864                                         if (name.equals(DATE_PARAMETER))
865                                                 dateParam = value;
866                                         else if (name.equals(AUTHORIZATION_PARAMETER))
867                                                 auth = value;
868
869                                         if (logger.isDebugEnabled())
870                                                 logger.debug(name + ":" + value);
871                                 } else {
872                                         // Fetch the timestamp used to guard against replay attacks.
873                                 if (dateParam == null) {
874                                         response.sendError(HttpServletResponse.SC_FORBIDDEN, "No Date parameter");
875                                         return;
876                                 }
877
878                                 long timestamp;
879                                         try {
880                                                 timestamp = DateUtil.parseDate(dateParam).getTime();
881                                         } catch (DateParseException e) {
882                                         response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
883                                         return;
884                                         }
885                                 if (!isTimeValid(timestamp)) {
886                                         response.sendError(HttpServletResponse.SC_FORBIDDEN);
887                                         return;
888                                 }
889
890                                         // Fetch the Authorization parameter and find the user specified in it.
891                                 if (auth == null) {
892                                         response.sendError(HttpServletResponse.SC_FORBIDDEN, "No Authorization parameter");
893                                         return;
894                                 }
895                                         String[] authParts = auth.split(" ");
896                                         if (authParts.length != 2) {
897                                         response.sendError(HttpServletResponse.SC_FORBIDDEN);
898                                         return;
899                                 }
900                                         String username = authParts[0];
901                                         String signature = authParts[1];
902                                         User user = null;
903                                         try {
904                                                 user = getService().findUser(username);
905                                         } catch (RpcException e) {
906                                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
907                                                 return;
908                                         }
909                                         if (user == null) {
910                                         response.sendError(HttpServletResponse.SC_FORBIDDEN);
911                                         return;
912                                 }
913                                         request.setAttribute(USER_ATTRIBUTE, user);
914
915                                         // Remove the servlet path from the request URI.
916                                         String p = request.getRequestURI();
917                                         String servletPath = request.getContextPath() + request.getServletPath();
918                                         p = p.substring(servletPath.length());
919                                         // Validate the signature in the Authorization parameter.
920                                         String data = request.getMethod() + dateParam + p;
921                                         if (!isSignatureValid(signature, user, data)) {
922                                         response.sendError(HttpServletResponse.SC_FORBIDDEN);
923                                         return;
924                                 }
925
926                                         progressListener.setUserId(user.getId());
927                                         progressListener.setFilename(fileName);
928                                         final String contentType = item.getContentType();
929
930                                         try {
931                                                 uploadedFile = getService().uploadFile(stream, user.getId());
932                                         } catch (IOException ex) {
933                                                 throw new GSSIOException(ex, false);
934                                         }
935                                         FileHeaderDTO fileDTO = null;
936                                         final File upf = uploadedFile;
937                                         final FileHeaderDTO f = file;
938                                         final User u = user;
939                                         if (file == null)
940                                                 fileDTO = new TransactionHelper<FileHeaderDTO>().tryExecute(new Callable<FileHeaderDTO>() {
941                                                         @Override
942                                                         public FileHeaderDTO call() throws Exception {
943                                                                 return getService().createFile(u.getId(), folder.getId(), fileName, contentType, upf.getCanonicalFile().length(), upf.getAbsolutePath());
944                                                         }
945                                                 });
946                                         else
947                                                 fileDTO = new TransactionHelper<FileHeaderDTO>().tryExecute(new Callable<FileHeaderDTO>() {
948                                                         @Override
949                                                         public FileHeaderDTO call() throws Exception {
950                                                                 return getService().updateFileContents(u.getId(), f.getId(), contentType, upf.getCanonicalFile().length(), upf.getAbsolutePath());
951                                                         }
952                                                 });
953                                         updateAccounting(owner, new Date(), fileDTO.getFileSize());
954                                         getService().removeFileUploadProgress(user.getId(), fileName);
955                                 }
956                         }
957                         // We can't return 204 here since GWT's onSubmitComplete won't fire.
958                         response.setContentType("text/html");
959             response.getWriter().print("<pre></pre>");
960                 } catch (FileUploadException e) {
961                         String error = "Error while uploading file";
962                         logger.error(error, e);
963                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
964                 } catch (GSSIOException e) {
965                         if (uploadedFile != null && uploadedFile.exists())
966                                 uploadedFile.delete();
967                         String error = "Error while uploading file";
968                         if (e.logAsError())
969                                 logger.error(error, e);
970                         else
971                                 logger.debug(error, e);
972                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
973                 } catch (DuplicateNameException e) {
974                         if (uploadedFile != null && uploadedFile.exists())
975                                 uploadedFile.delete();
976                         String error = "The specified file name already exists in this folder";
977                         logger.error(error, e);
978                         response.sendError(HttpServletResponse.SC_CONFLICT, error);
979
980                 } catch (InsufficientPermissionsException e) {
981                         if (uploadedFile != null && uploadedFile.exists())
982                                 uploadedFile.delete();
983                         String error = "You don't have the necessary permissions";
984                         logger.error(error, e);
985                         response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, error);
986
987                 } catch (QuotaExceededException e) {
988                         if (uploadedFile != null && uploadedFile.exists())
989                                 uploadedFile.delete();
990                         String error = "Not enough free space available";
991                         if (logger.isDebugEnabled())
992                                 logger.debug(error, e);
993                         response.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, error);
994
995                 } catch (ObjectNotFoundException e) {
996                         if (uploadedFile != null && uploadedFile.exists())
997                                 uploadedFile.delete();
998                         String error = "A specified object was not found";
999                         logger.error(error, e);
1000                         response.sendError(HttpServletResponse.SC_NOT_FOUND, error);
1001                 } catch (RpcException e) {
1002                         if (uploadedFile != null && uploadedFile.exists())
1003                                 uploadedFile.delete();
1004                         String error = "An error occurred while communicating with the service";
1005                         logger.error(error, e);
1006                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
1007                 } catch (Exception e) {
1008                         if (uploadedFile != null && uploadedFile.exists())
1009                                 uploadedFile.delete();
1010                         String error = "An internal server error occurred";
1011                         logger.error(error, e);
1012                         response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, error);
1013                 }
1014         }
1015
1016         /**
1017          * Move the resource in the specified path to the specified destination.
1018          *
1019          * @param req the HTTP request
1020          * @param resp the HTTP response
1021          * @param path the path of the resource
1022          * @param moveTo the destination of the move procedure
1023          * @throws IOException if an input/output error occurs
1024          */
1025         private void moveResource(HttpServletRequest req, HttpServletResponse resp, String path, String moveTo) throws IOException {
1026                 final User user = getUser(req);
1027                 User owner = getOwner(req);
1028                 Object resource = null;
1029                 try {
1030                         resource = getService().getResourceAtPath(owner.getId(), path, true);
1031                 } catch (ObjectNotFoundException e) {
1032                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
1033                         return;
1034                 } catch (RpcException e) {
1035                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1036                         return;
1037                 }
1038
1039         String destination = null;
1040         User destOwner = null;
1041                 boolean exists = true;
1042                 try {
1043                         destination = getDestinationPath(req, encodePath(moveTo));
1044                         destination = URLDecoder.decode(destination, "UTF-8");
1045                         destOwner = getDestinationOwner(req);
1046                         getService().getResourceAtPath(destOwner.getId(), destination, true);
1047                 } catch (ObjectNotFoundException e) {
1048                         exists = false;
1049                 } catch (URISyntaxException e) {
1050                         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
1051                         return;
1052                 } catch (RpcException e) {
1053                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, destination);
1054                         return;
1055                 }
1056                 if (exists) {
1057                         resp.sendError(HttpServletResponse.SC_CONFLICT, destination + " already exists");
1058                         return;
1059                 }
1060
1061                 try {
1062                         final User dOwner = destOwner;
1063                         final String dest = destination;
1064                         if (resource instanceof FolderDTO) {
1065                                 final FolderDTO folder = (FolderDTO) resource;
1066                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1067                                         @Override
1068                                         public Void call() throws Exception {
1069                                                 getService().moveFolderToPath(user.getId(), dOwner.getId(), folder.getId(), dest);
1070                                                 return null;
1071                                         }
1072                                 });
1073                         } else {
1074                                 final FileHeaderDTO file = (FileHeaderDTO) resource;
1075                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1076                                         @Override
1077                                         public Void call() throws Exception {
1078                                                 getService().moveFileToPath(user.getId(), dOwner.getId(), file.getId(), dest);
1079                                                 return null;
1080                                         }
1081                                 });
1082
1083                         }
1084                 } catch (InsufficientPermissionsException e) {
1085                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1086                 } catch (ObjectNotFoundException e) {
1087                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
1088                 } catch (RpcException e) {
1089                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, destination);
1090                 } catch (DuplicateNameException e) {
1091                         resp.sendError(HttpServletResponse.SC_CONFLICT, e.getMessage());
1092                 } catch (GSSIOException e) {
1093                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
1094                 } catch (QuotaExceededException e) {
1095                         resp.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, e.getMessage());
1096                 } catch (Exception e) {
1097                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, destination);
1098                 }
1099         }
1100
1101         /**
1102          * Copy the resource in the specified path to the specified destination.
1103          *
1104          * @param req the HTTP request
1105          * @param resp the HTTP response
1106          * @param path the path of the resource
1107          * @param copyTo the destination of the copy procedure
1108          * @throws IOException if an input/output error occurs
1109          */
1110         private void copyResource(HttpServletRequest req, HttpServletResponse resp, String path, String copyTo) throws IOException {
1111                 final User user = getUser(req);
1112                 User owner = getOwner(req);
1113                 Object resource = null;
1114                 try {
1115                         resource = getService().getResourceAtPath(owner.getId(), path, true);
1116                 } catch (ObjectNotFoundException e) {
1117                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
1118                         return;
1119                 } catch (RpcException e) {
1120                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1121                         return;
1122                 }
1123
1124         String destination = null;
1125         User destOwner = null;
1126                 boolean exists = true;
1127                 try {
1128                         String destinationEncoded = getDestinationPath(req, encodePath(copyTo));
1129                         destination = URLDecoder.decode(destinationEncoded, "UTF-8");
1130                         destOwner = getDestinationOwner(req);
1131                         getService().getResourceAtPath(destOwner.getId(), destinationEncoded, true);
1132                 } catch (ObjectNotFoundException e) {
1133                         exists = false;
1134                 } catch (URISyntaxException e) {
1135                         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
1136                         return;
1137                 } catch (RpcException e) {
1138                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, destination);
1139                         return;
1140                 }
1141                 if (exists) {
1142                         resp.sendError(HttpServletResponse.SC_CONFLICT, destination + " already exists");
1143                         return;
1144                 }
1145
1146                 try {
1147                         final User dOwner = destOwner;
1148                         final String dest = destination;
1149                         if (resource instanceof FolderDTO) {
1150                                 final FolderDTO folder = (FolderDTO) resource;
1151                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1152                                         @Override
1153                                         public Void call() throws Exception {
1154                                                 getService().copyFolderStructureToPath(user.getId(), dOwner.getId(), folder.getId(), dest);
1155                                                 return null;
1156                                         }
1157                                 });
1158                         } else {
1159                                 final FileHeaderDTO file = (FileHeaderDTO) resource;
1160                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1161                                         @Override
1162                                         public Void call() throws Exception {
1163                                                 getService().copyFileToPath(user.getId(), dOwner.getId(), file.getId(), dest);
1164                                                 return null;
1165                                         }
1166                                 });
1167                         }
1168                 } catch (InsufficientPermissionsException e) {
1169                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1170                 } catch (ObjectNotFoundException e) {
1171                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
1172                 } catch (RpcException e) {
1173                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, destination);
1174                 } catch (DuplicateNameException e) {
1175                         resp.sendError(HttpServletResponse.SC_CONFLICT, e.getMessage());
1176                 } catch (GSSIOException e) {
1177                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
1178                 } catch (QuotaExceededException e) {
1179                         resp.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, e.getMessage());
1180                 } catch (Exception e) {
1181                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, destination);
1182                 }
1183         }
1184
1185         private String encodePath(String path) throws UnsupportedEncodingException{
1186                 StringTokenizer str = new StringTokenizer(path, "/:", true);
1187                 String result = new String();
1188                 while(str.hasMoreTokens()){
1189                         String token = str.nextToken();
1190                         if(!token.equals("/") && !token.equals(":"))
1191                                 token = URLEncoder.encode(token,"UTF-8");
1192                         result = result + token;
1193                 }
1194                 return result;
1195         }
1196         /**
1197          * A helper method that extracts the relative resource path,
1198          * after removing the 'files' namespace.
1199          * The path returned is <i>not</i> URL-decoded.
1200          *
1201          * @param req the HTTP request
1202          * @param path the specified path
1203          * @return the path relative to the root folder
1204          * @throws URISyntaxException
1205          * @throws RpcException in case an error occurs while communicating
1206          *                                              with the backend
1207          * @throws UnsupportedEncodingException
1208          */
1209         private String getDestinationPath(HttpServletRequest req, String path) throws URISyntaxException, RpcException, UnsupportedEncodingException {
1210                 URI uri = new URI(path);
1211                 String dest = uri.getRawPath();
1212                 // Remove the context path from the destination URI.
1213                 String contextPath = req.getContextPath();
1214                 if (!dest.startsWith(contextPath))
1215                         throw new URISyntaxException(dest, "Destination path does not start with " + contextPath);
1216                 dest = dest.substring(contextPath.length());
1217                 // Remove the servlet path from the destination URI.
1218                 String servletPath = req.getServletPath();
1219                 if (!dest.startsWith(servletPath))
1220                         throw new URISyntaxException(dest, "Destination path does not start with " + servletPath);
1221                 dest = dest.substring(servletPath.length());
1222         // Strip the username part
1223                 if (dest.length() < 2)
1224                         throw new URISyntaxException(dest, "No username in the destination URI");
1225                 int slash = dest.substring(1).indexOf('/');
1226                 if (slash == -1)
1227                         throw new URISyntaxException(dest, "No username in the destination URI");
1228                 // Decode the user to get the proper characters (mainly the @)
1229                 String owner = URLDecoder.decode(dest.substring(1, slash + 1), "UTF-8");
1230                 User o;
1231                 o = getService().findUser(owner);
1232                 if (o == null)
1233                         throw new URISyntaxException(dest, "User " + owner + " not found");
1234
1235                 req.setAttribute(DESTINATION_OWNER_ATTRIBUTE, o);
1236                 dest = dest.substring(slash + 1);
1237
1238                 // Chop the resource namespace part
1239                 dest = dest.substring(RequestHandler.PATH_FILES.length());
1240
1241         dest = dest.endsWith("/")? dest: dest + '/';
1242                 return dest;
1243         }
1244
1245         /**
1246          * Move the resource in the specified path to the trash bin.
1247          *
1248          * @param req the HTTP request
1249          * @param resp the HTTP response
1250          * @param path the path of the resource
1251          * @throws IOException if an input/output error occurs
1252          */
1253         private void trashResource(HttpServletRequest req, HttpServletResponse resp, String path) throws IOException {
1254                 final User user = getUser(req);
1255                 User owner = getOwner(req);
1256                 Object resource = null;
1257                 try {
1258                         resource = getService().getResourceAtPath(owner.getId(), path, true);
1259                 } catch (ObjectNotFoundException e) {
1260                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
1261                         return;
1262                 } catch (RpcException e) {
1263                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1264                         return;
1265                 }
1266
1267                 try {
1268                         if (resource instanceof FolderDTO) {
1269                                 final FolderDTO folder = (FolderDTO) resource;
1270                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1271                                         @Override
1272                                         public Void call() throws Exception {
1273                                                 getService().moveFolderToTrash(user.getId(), folder.getId());
1274                                                 return null;
1275                                         }
1276                                 });
1277                         } else {
1278                                 final FileHeaderDTO file = (FileHeaderDTO) resource;
1279                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1280                                         @Override
1281                                         public Void call() throws Exception {
1282                                                 getService().moveFileToTrash(user.getId(), file.getId());
1283                                                 return null;
1284                                         }
1285                                 });
1286                         }
1287                 } catch (InsufficientPermissionsException e) {
1288                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1289                 } catch (ObjectNotFoundException e) {
1290                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
1291                 } catch (RpcException e) {
1292                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1293                 } catch (Exception e) {
1294                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1295                 }
1296         }
1297
1298         /**
1299          * Restore the resource in the specified path from the trash bin.
1300          *
1301          * @param req the HTTP request
1302          * @param resp the HTTP response
1303          * @param path the path of the resource
1304          * @throws IOException if an input/output error occurs
1305          */
1306         private void restoreResource(HttpServletRequest req, HttpServletResponse resp, String path) throws IOException {
1307                 final User user = getUser(req);
1308                 User owner = getOwner(req);
1309                 Object resource = null;
1310                 try {
1311                         resource = getService().getResourceAtPath(owner.getId(), path, false);
1312                 } catch (ObjectNotFoundException e) {
1313                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
1314                         return;
1315                 } catch (RpcException e) {
1316                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1317                         return;
1318                 }
1319
1320                 try {
1321                         if (resource instanceof FolderDTO) {
1322                                 final FolderDTO folder = (FolderDTO) resource;
1323                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1324                                         @Override
1325                                         public Void call() throws Exception {
1326                                                 getService().removeFolderFromTrash(user.getId(), folder.getId());
1327                                                 return null;
1328                                         }
1329                                 });
1330                         } else {
1331                                 final FileHeaderDTO file = (FileHeaderDTO) resource;
1332                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1333                                         @Override
1334                                         public Void call() throws Exception {
1335                                                 getService().removeFileFromTrash(user.getId(), file.getId());
1336                                                 return null;
1337                                         }
1338                                 });
1339                         }
1340                 } catch (InsufficientPermissionsException e) {
1341                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1342                 } catch (ObjectNotFoundException e) {
1343                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
1344                 } catch (RpcException e) {
1345                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1346                 } catch (Exception e) {
1347                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1348                 }
1349         }
1350
1351         /**
1352          * Update the resource in the specified path.
1353          *
1354          * @param req the HTTP request
1355          * @param resp the HTTP response
1356          * @param path the path of the resource
1357          * @throws IOException if an input/output error occurs
1358          */
1359         private void updateResource(HttpServletRequest req, HttpServletResponse resp, String path) throws IOException {
1360                 final User user = getUser(req);
1361                 User owner = getOwner(req);
1362                 Object resource = null;
1363                 try {
1364                         resource = getService().getResourceAtPath(owner.getId(), path, false);
1365                 } catch (ObjectNotFoundException e) {
1366                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, path);
1367                         return;
1368                 } catch (RpcException e) {
1369                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1370                         return;
1371                 }
1372                 StringBuffer input = new StringBuffer();
1373                 JSONObject json = null;
1374                 if (req.getContentType().startsWith("application/x-www-form-urlencoded"))
1375                         input.append(req.getParameter(RESOURCE_UPDATE_PARAMETER));
1376                 else {
1377                         // Assume application/json
1378                         BufferedReader reader = new BufferedReader(new InputStreamReader(req.getInputStream(),"UTF-8"));
1379                         String line = null;
1380                         while ((line = reader.readLine()) != null)
1381                                 input.append(line);
1382                         reader.close();
1383                 }
1384                 try {
1385                         json = new JSONObject(input.toString());
1386                         if (logger.isDebugEnabled())
1387                                 logger.debug("JSON update: " + json);
1388                         if (resource instanceof FolderDTO) {
1389                                 final FolderDTO folder = (FolderDTO) resource;
1390                                 String name = json.optString("name");
1391                                 if (!name.isEmpty())
1392                                         try {
1393                                                 name = URLDecoder.decode(name, "UTF-8");
1394                                         } catch (IllegalArgumentException e) {
1395                                                 resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
1396                                                 return;
1397                                         }
1398                                 JSONArray permissions = json.optJSONArray("permissions");
1399                                 Set<PermissionDTO> perms = null;
1400                                 if (permissions != null)
1401                                         perms = parsePermissions(user, permissions);
1402                                 if (!name.isEmpty() || permissions != null) {
1403                                         final String fName = name.isEmpty()? null: name;
1404                                         final Set<PermissionDTO> fPerms = perms;
1405                                         FolderDTO folderUpdated = new TransactionHelper<FolderDTO>().tryExecute(new Callable<FolderDTO>() {
1406                                                 @Override
1407                                                 public FolderDTO call() throws Exception {
1408                                                         return getService().updateFolder(user.getId(), folder.getId(), fName, fPerms);
1409                                                 }
1410
1411                                         });
1412                                         resp.getWriter().println(getNewUrl(req, folderUpdated));
1413                                 }
1414                         } else {
1415                                 final FileHeaderDTO file = (FileHeaderDTO) resource;
1416                                 String name = null;
1417                                 if (json.opt("name") != null)
1418                                         name = json.optString("name");
1419                                 Long modificationDate = null;
1420                                 if (json.optLong("modificationDate") != 0)
1421                                         modificationDate = json.optLong("modificationDate");
1422                                 Boolean versioned = null;
1423                                 if (json.opt("versioned") != null)
1424                                         versioned = json.getBoolean("versioned");
1425                                 JSONArray tagset = json.optJSONArray("tags");
1426                                 String tags = null;
1427                                 StringBuffer t = new StringBuffer();
1428                                 if (tagset != null) {
1429                                         for (int i = 0; i < tagset.length(); i++)
1430                                                 t.append(tagset.getString(i) + ',');
1431                                         tags = t.toString();
1432                                 }
1433                                 JSONArray permissions = json.optJSONArray("permissions");
1434                                 Set<PermissionDTO> perms = null;
1435                                 if (permissions != null)
1436                                         perms = parsePermissions(user, permissions);
1437                                 Boolean readForAll = null;
1438                                 if (json.opt("readForAll") != null)
1439                                         readForAll = json.optBoolean("readForAll");
1440                                 if (name != null || tags != null || modificationDate != null
1441                                                         || versioned != null || perms != null
1442                                                         || readForAll != null) {
1443                                         final String fName = name;
1444                                         final String fTags = tags;
1445                                         final Date mDate = modificationDate != null? new Date(modificationDate): null;
1446                                         final Boolean fVersioned = versioned;
1447                                         final Boolean fReadForAll = readForAll;
1448                                         final Set<PermissionDTO> fPerms = perms;
1449                                         new TransactionHelper<Object>().tryExecute(new Callable<Object>() {
1450                                                 @Override
1451                                                 public Object call() throws Exception {
1452                                                         getService().updateFile(user.getId(), file.getId(),
1453                                                                                 fName, fTags, mDate, fVersioned,
1454                                                                                 fReadForAll, fPerms);
1455                                                         return null;
1456                                                 }
1457
1458                                         });
1459                                 }
1460                         }
1461                 } catch (JSONException e) {
1462                         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
1463                 } catch (InsufficientPermissionsException e) {
1464                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1465                 } catch (ObjectNotFoundException e) {
1466                         resp.sendError(HttpServletResponse.SC_NOT_FOUND, e.getMessage());
1467                 } catch (DuplicateNameException e) {
1468                         resp.sendError(HttpServletResponse.SC_CONFLICT, e.getMessage());
1469                 } catch (RpcException e) {
1470                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1471                 } catch (Exception e) {
1472                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1473                         return;
1474                 }
1475         }
1476
1477         /**
1478          * Returns the new URL of an updated folder.
1479          */
1480         private String getNewUrl(HttpServletRequest req, FolderDTO folder) throws UnsupportedEncodingException {
1481                 String parentUrl =URLDecoder.decode(getContextPath(req, true),"UTF-8");
1482                 String fpath = URLDecoder.decode(req.getPathInfo(), "UTF-8");
1483                 if (parentUrl.indexOf(fpath) != -1)
1484                         parentUrl = parentUrl.substring(0, parentUrl.indexOf(fpath));
1485                 if(!parentUrl.endsWith("/"))
1486                         parentUrl = parentUrl+"/";
1487                 parentUrl = parentUrl+folder.getOwner().getUsername()+PATH_FILES+folder.getPath();
1488                 return parentUrl;
1489         }
1490
1491         /**
1492          * Helper method to convert a JSON array of permissions into a set of
1493          * PermissionDTO objects.
1494          *
1495          * @param user the current user
1496          * @param permissions the JSON array to parse
1497          * @return the parsed set of permissions
1498          * @throws JSONException if there was an error parsing the JSON object
1499          * @throws RpcException if there was an error communicating with the EJB
1500          * @throws ObjectNotFoundException if the user could not be found
1501          * @throws UnsupportedEncodingException
1502          */
1503         private Set<PermissionDTO> parsePermissions(User user, JSONArray permissions)
1504                         throws JSONException, RpcException, ObjectNotFoundException, UnsupportedEncodingException {
1505                 if (permissions == null)
1506                         return null;
1507                 Set<PermissionDTO> perms = new HashSet<PermissionDTO>();
1508                 for (int i = 0; i < permissions.length(); i++) {
1509                         JSONObject j = permissions.getJSONObject(i);
1510                         PermissionDTO perm = new PermissionDTO();
1511                         perm.setModifyACL(j.optBoolean("modifyACL"));
1512                         perm.setRead(j.optBoolean("read"));
1513                         perm.setWrite(j.optBoolean("write"));
1514                         String permUser = j.optString("user");
1515                         if (!permUser.isEmpty()) {
1516                                 User u = getService().findUser(permUser);
1517                                 if (u == null)
1518                                         throw new ObjectNotFoundException("User " + permUser + " not found");
1519                                 perm.setUser(u.getDTO());
1520                         }
1521                         // 31/8/2009: Add optional groupUri which takes priority if it exists
1522                         String permGroupUri = j.optString("groupUri");
1523                         String permGroup = j.optString("group");
1524                         if (!permGroupUri.isEmpty()) {
1525                                 String[] names = permGroupUri.split("/");
1526                                 String grp = URLDecoder.decode(names[names.length - 1], "UTF-8");
1527                                 String usr = URLDecoder.decode(names[names.length - 3], "UTF-8");
1528                                 User u = getService().findUser(usr);
1529                                 if (u == null)
1530                                         throw new ObjectNotFoundException("User " + permUser + " not found");
1531                                 GroupDTO g = getService().getGroup(u.getId(), grp);
1532                                 perm.setGroup(g);
1533                         }
1534                         else if (!permGroup.isEmpty()) {
1535                                 GroupDTO g = getService().getGroup(user.getId(), permGroup);
1536                                 perm.setGroup(g);
1537                         }
1538                         if (permUser.isEmpty() && permGroupUri.isEmpty() && permGroup.isEmpty())
1539                                 throw new JSONException("A permission must correspond to either a user or a group");
1540                         perms.add(perm);
1541                 }
1542                 return perms;
1543         }
1544
1545         /**
1546          * Creates a new folder with the specified name under the folder in the provided path.
1547          *
1548          * @param req the HTTP request
1549          * @param resp the HTTP response
1550          * @param path the parent folder path
1551          * @param folderName the name of the new folder
1552          * @throws IOException if an input/output error occurs
1553          */
1554         private void createFolder(HttpServletRequest req, HttpServletResponse resp, String path, final String folderName) throws IOException {
1555                 if (logger.isDebugEnabled())
1556                         logger.debug("Creating folder " + folderName + " in '" + path);
1557
1558         final User user = getUser(req);
1559         User owner = getOwner(req);
1560         boolean exists = true;
1561         try {
1562                 getService().getResourceAtPath(owner.getId(), path + folderName, false);
1563         } catch (ObjectNotFoundException e) {
1564             exists = false;
1565         } catch (RpcException e) {
1566                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path + folderName);
1567                         return;
1568                 }
1569
1570         if (exists) {
1571             resp.addHeader("Allow", METHOD_GET + ", " + METHOD_DELETE +
1572                                 ", " + METHOD_HEAD);
1573             resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1574             return;
1575         }
1576
1577                 Object parent;
1578                 try {
1579                         parent = getService().getResourceAtPath(owner.getId(), path, true);
1580                 } catch (ObjectNotFoundException e) {
1581                         resp.sendError(HttpServletResponse.SC_CONFLICT);
1582                         return;
1583                 } catch (RpcException e) {
1584                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path + folderName);
1585                         return;
1586                 }
1587                 try {
1588                         if (parent instanceof FolderDTO) {
1589                                 final FolderDTO folder = (FolderDTO) parent;
1590                                 FolderDTO newFolder = new TransactionHelper<FolderDTO>().tryExecute(new Callable<FolderDTO>() {
1591                                         @Override
1592                                         public FolderDTO call() throws Exception {
1593                                                 return getService().createFolder(user.getId(), folder.getId(), folderName);
1594                                         }
1595
1596                                 });
1597                         String newResource = getApiRoot() + newFolder.getURI();
1598                         resp.setHeader("Location", newResource);
1599                         resp.setContentType("text/plain");
1600                     PrintWriter out = resp.getWriter();
1601                     out.println(newResource);
1602                         } else {
1603                                 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1604                         return;
1605                         }
1606                 } catch (DuplicateNameException e) {
1607                         resp.sendError(HttpServletResponse.SC_CONFLICT);
1608                 return;
1609                 } catch (InsufficientPermissionsException e) {
1610                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1611                 return;
1612                 } catch (ObjectNotFoundException e) {
1613                         resp.sendError(HttpServletResponse.SC_CONFLICT);
1614                         return;
1615                 } catch (RpcException e) {
1616                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path + folderName);
1617                         return;
1618                 } catch (Exception e) {
1619                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1620                         return;
1621                 }
1622         resp.setStatus(HttpServletResponse.SC_CREATED);
1623         }
1624
1625         /**
1626          * @param req
1627          * @param resp
1628          * @throws IOException
1629          * @throws FileNotFoundException
1630          */
1631         void putResource(HttpServletRequest req, HttpServletResponse resp) throws IOException, FileNotFoundException {
1632         String path = getInnerPath(req, PATH_FILES);
1633                 try {
1634                 path = URLDecoder.decode(path, "UTF-8");
1635                 } catch (IllegalArgumentException e) {
1636                         resp.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
1637                         return;
1638                 }
1639         if (logger.isDebugEnabled())
1640                         logger.debug("Updating resource: " + path);
1641
1642         final User user = getUser(req);
1643         User owner = getOwner(req);
1644         boolean exists = true;
1645         Object resource = null;
1646         FileHeaderDTO file = null;
1647         try {
1648                 resource = getService().getResourceAtPath(owner.getId(), path, false);
1649         } catch (ObjectNotFoundException e) {
1650             exists = false;
1651         } catch (RpcException e) {
1652                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1653                         return;
1654                 }
1655
1656         if (exists)
1657                         if (resource instanceof FileHeaderDTO)
1658                         file = (FileHeaderDTO) resource;
1659                         else {
1660                         resp.sendError(HttpServletResponse.SC_CONFLICT, path + " is a folder");
1661                         return;
1662                 }
1663         boolean result = true;
1664
1665         // Temporary content file used to support partial PUT.
1666         File contentFile = null;
1667
1668         Range range = parseContentRange(req, resp);
1669
1670         InputStream resourceInputStream = null;
1671
1672         // Append data specified in ranges to existing content for this
1673         // resource - create a temporary file on the local filesystem to
1674         // perform this operation.
1675         // Assume just one range is specified for now
1676         if (range != null) {
1677             try {
1678                                 contentFile = executePartialPut(req, range, path);
1679                         } catch (RpcException e) {
1680                                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1681                                 return;
1682                         } catch (ObjectNotFoundException e) {
1683                                 resp.sendError(HttpServletResponse.SC_CONFLICT);
1684                         return;
1685                         } catch (InsufficientPermissionsException e) {
1686                                 resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1687                         return;
1688                         }
1689             resourceInputStream = new FileInputStream(contentFile);
1690         } else
1691                         resourceInputStream = req.getInputStream();
1692
1693         try {
1694                 FolderDTO folder = null;
1695                 Object parent = getService().getResourceAtPath(owner.getId(), getParentPath(path), true);
1696                 if (!(parent instanceof FolderDTO)) {
1697                         resp.sendError(HttpServletResponse.SC_CONFLICT);
1698                         return;
1699                 }
1700                 folder = (FolderDTO) parent;
1701                 final String name = getLastElement(path);
1702                 final String mimeType = context.getMimeType(name);
1703                 File uploadedFile = null;
1704                 try {
1705                                 uploadedFile = getService().uploadFile(resourceInputStream, user.getId());
1706                         } catch (IOException ex) {
1707                                 throw new GSSIOException(ex, false);
1708                         }
1709                 FileHeaderDTO fileDTO = null;
1710                 final File uploadedf = uploadedFile;
1711                         final FolderDTO parentf = folder;
1712                         final FileHeaderDTO f = file;
1713             if (exists)
1714                 fileDTO = new TransactionHelper<FileHeaderDTO>().tryExecute(new Callable<FileHeaderDTO>() {
1715                                         @Override
1716                                         public FileHeaderDTO call() throws Exception {
1717                                                 return getService().updateFileContents(user.getId(), f.getId(), mimeType, uploadedf.getCanonicalFile().length(), uploadedf.getAbsolutePath());
1718                                         }
1719                                 });
1720                         else
1721                                 fileDTO = new TransactionHelper<FileHeaderDTO>().tryExecute(new Callable<FileHeaderDTO>() {
1722                                         @Override
1723                                         public FileHeaderDTO call() throws Exception {
1724                                                 return getService().createFile(user.getId(), parentf.getId(), name, mimeType, uploadedf.getCanonicalFile().length(), uploadedf.getAbsolutePath());
1725                                         }
1726
1727                                 });
1728             updateAccounting(owner, new Date(), fileDTO.getFileSize());
1729                         getService().removeFileUploadProgress(user.getId(), fileDTO.getName());
1730         } catch(ObjectNotFoundException e) {
1731             result = false;
1732         } catch (RpcException e) {
1733                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1734                         return;
1735         } catch (IOException e) {
1736                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1737                         return;
1738                 } catch (GSSIOException e) {
1739                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1740                         return;
1741                 } catch (DuplicateNameException e) {
1742                         resp.sendError(HttpServletResponse.SC_CONFLICT);
1743                 return;
1744                 } catch (InsufficientPermissionsException e) {
1745                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1746                 return;
1747                 } catch (QuotaExceededException e) {
1748                         resp.sendError(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, e.getMessage());
1749                 return;
1750                 } catch (Exception e) {
1751                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path);
1752                         return;
1753                 }
1754
1755         if (result) {
1756             if (exists)
1757                                 resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
1758                         else
1759                                 resp.setStatus(HttpServletResponse.SC_CREATED);
1760         } else
1761                         resp.sendError(HttpServletResponse.SC_CONFLICT);
1762         }
1763
1764     /**
1765      * Delete a resource.
1766      *
1767      * @param req The servlet request we are processing
1768      * @param resp The servlet response we are processing
1769          * @throws IOException if the response cannot be sent
1770      */
1771     void deleteResource(HttpServletRequest req, HttpServletResponse resp) throws IOException {
1772         String path = getInnerPath(req, PATH_FILES);
1773         if (logger.isDebugEnabled())
1774                         logger.debug("Deleting resource '" + path);
1775         path = URLDecoder.decode(path, "UTF-8");
1776         final User user = getUser(req);
1777         User owner = getOwner(req);
1778         boolean exists = true;
1779         Object object = null;
1780         try {
1781                 object = getService().getResourceAtPath(owner.getId(), path, false);
1782         } catch (ObjectNotFoundException e) {
1783                 exists = false;
1784         } catch (RpcException e) {
1785                 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
1786                         return;
1787                 }
1788
1789         if (!exists) {
1790                 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
1791                 return;
1792         }
1793
1794         FolderDTO folder = null;
1795         FileHeaderDTO file = null;
1796         if (object instanceof FolderDTO)
1797                 folder = (FolderDTO) object;
1798         else
1799                 file = (FileHeaderDTO) object;
1800
1801         if (file != null)
1802                         try {
1803                                 final FileHeaderDTO f = file;
1804                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1805                                         @Override
1806                                         public Void call() throws Exception {
1807                                                 getService().deleteFile(user.getId(), f.getId());
1808                                                 return null;
1809                                         }
1810                                 });
1811                 } catch (InsufficientPermissionsException e) {
1812                         resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1813                                 return;
1814                 } catch (ObjectNotFoundException e) {
1815                         // Although we had already found the object, it was
1816                         // probably deleted from another thread.
1817                         resp.sendError(HttpServletResponse.SC_NOT_FOUND);
1818                         return;
1819                 } catch (RpcException e) {
1820                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
1821                         return;
1822                 } catch (Exception e) {
1823                         resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
1824                         return;
1825                 }
1826                 else if (folder != null)
1827                         try {
1828                                 final FolderDTO fo = folder;
1829                                 new TransactionHelper<Void>().tryExecute(new Callable<Void>() {
1830                                         @Override
1831                                         public Void call() throws Exception {
1832                                                 getService().deleteFolder(user.getId(), fo.getId());
1833                                                 return null;
1834                                         }
1835                                 });
1836                 } catch (InsufficientPermissionsException e) {
1837                         resp.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
1838                         return;
1839                 } catch (ObjectNotFoundException e) {
1840                         resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
1841                         return;
1842                 } catch (RpcException e) {
1843                         resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
1844                         return;
1845                 } catch (Exception e) {
1846                         resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
1847                         return;
1848                 }
1849                 resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
1850         return;
1851     }
1852
1853         /**
1854      * Return an InputStream to a JSON representation of the contents
1855      * of this directory.
1856      *
1857          * @param user the user that made the request
1858      * @param folder the specified directory
1859      * @return an input stream with the rendered contents
1860          * @throws IOException if the response cannot be sent
1861      * @throws ServletException
1862          * @throws InsufficientPermissionsException if the user does not have
1863          *                      the necessary privileges to read the directory
1864      */
1865     private InputStream renderJson(User user, FolderDTO folder) throws IOException,
1866                 ServletException, InsufficientPermissionsException {
1867         JSONObject json = new JSONObject();
1868         try {
1869                         json.put("name", folder.getName()).
1870                                         put("owner", folder.getOwner().getUsername()).
1871                                         put("createdBy", folder.getAuditInfo().getCreatedBy().getUsername()).
1872                                         put("creationDate", folder.getAuditInfo().getCreationDate().getTime()).
1873                                         put("deleted", folder.isDeleted());
1874                         if (folder.getAuditInfo().getModifiedBy() != null)
1875                                 json.put("modifiedBy", folder.getAuditInfo().getModifiedBy().getUsername()).
1876                                                 put("modificationDate", folder.getAuditInfo().getModificationDate().getTime());
1877                         if (folder.getParent() != null) {
1878                                 JSONObject j = new JSONObject();
1879                                 j.put("uri", getApiRoot() + folder.getParent().getURI());
1880                                 j.put("name", folder.getParent().getName());
1881                                 json.put("parent", j);
1882                         }
1883                 List<JSONObject> subfolders = new ArrayList<JSONObject>();
1884                 for (FolderDTO f: folder.getSubfolders())
1885                                 if (!f.isDeleted()) {
1886                                         JSONObject j = new JSONObject();
1887                                         j.put("name", f.getName()).
1888                                                 put("uri", getApiRoot() + f.getURI());
1889                                         subfolders.add(j);
1890                                 }
1891                 json.put("folders", subfolders);
1892                 List<JSONObject> files = new ArrayList<JSONObject>();
1893                 List<FileHeaderDTO> fileHeaders = getService().getFiles(user.getId(), folder.getId(), false);
1894                 for (FileHeaderDTO f: fileHeaders) {
1895                         JSONObject j = new JSONObject();
1896                                 j.put("name", f.getName()).
1897                                         put("owner", f.getOwner().getUsername()).
1898                                         put("deleted", f.isDeleted()).
1899                                         put("version", f.getVersion()).
1900                                         put("content", f.getMimeType()).
1901                                         put("size", f.getFileSize()).
1902                                         put("creationDate", f.getAuditInfo().getCreationDate().getTime()).
1903                                         put("path", f.getFolder().getPath()).
1904                                         put("uri", getApiRoot() + f.getURI());
1905                                 if (f.getAuditInfo().getModificationDate() != null)
1906                                         j.put("modificationDate", f.getAuditInfo().getModificationDate().getTime());
1907                                 files.add(j);
1908                 }
1909                 json.put("files", files);
1910                 Set<PermissionDTO> perms = getService().getFolderPermissions(user.getId(), folder.getId());
1911                 json.put("permissions", renderJson(perms));
1912                 } catch (JSONException e) {
1913                         throw new ServletException(e);
1914                 } catch (ObjectNotFoundException e) {
1915                         throw new ServletException(e);
1916                 } catch (RpcException e) {
1917                         throw new ServletException(e);
1918                 }
1919
1920         // Prepare a writer to a buffered area
1921         ByteArrayOutputStream stream = new ByteArrayOutputStream();
1922         OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
1923         PrintWriter writer = new PrintWriter(osWriter);
1924
1925         // Return an input stream to the underlying bytes
1926         writer.write(json.toString());
1927         writer.flush();
1928         return new ByteArrayInputStream(stream.toByteArray());
1929     }
1930
1931         /**
1932      * Return a String with a JSON representation of the metadata
1933      * of the specified folder.
1934          * @throws RpcException
1935          * @throws InsufficientPermissionsException
1936          * @throws ObjectNotFoundException
1937      */
1938     private String renderJsonMetadata(User user, FolderDTO folder)
1939                 throws ServletException, InsufficientPermissionsException {
1940         // Check if the user has read permission.
1941                 try {
1942                         if (!getService().canReadFolder(user.getId(), folder.getId()))
1943                                 throw new InsufficientPermissionsException();
1944                 } catch (ObjectNotFoundException e) {
1945                         throw new ServletException(e);
1946                 } catch (RpcException e) {
1947                         throw new ServletException(e);
1948                 }
1949
1950         JSONObject json = new JSONObject();
1951         try {
1952                         json.put("name", folder.getName()).
1953                         put("owner", folder.getOwner().getUsername()).
1954                         put("createdBy", folder.getAuditInfo().getCreatedBy().getUsername()).
1955                         put("creationDate", folder.getAuditInfo().getCreationDate().getTime()).
1956                         put("deleted", folder.isDeleted());
1957                         if (folder.getAuditInfo().getModifiedBy() != null)
1958                                 json.put("modifiedBy", folder.getAuditInfo().getModifiedBy().getUsername()).
1959                                                 put("modificationDate", folder.getAuditInfo().getModificationDate().getTime());
1960                 } catch (JSONException e) {
1961                         throw new ServletException(e);
1962                 }
1963         return json.toString();
1964     }
1965
1966         /**
1967      * Return a String with a JSON representation of the metadata
1968      * of the specified file. If an old file body is provided, then
1969      * the metadata of that particular version will be returned.
1970      *
1971          * @param user the user that made the request
1972      * @param file the specified file header
1973      * @param oldBody the version number
1974      * @return the JSON-encoded file
1975      * @throws ServletException
1976          * @throws InsufficientPermissionsException if the user does not have
1977          *                      the necessary privileges to read the directory
1978      */
1979     private String renderJson(User user, FileHeaderDTO file, FileBodyDTO oldBody)
1980                 throws ServletException, InsufficientPermissionsException {
1981         JSONObject json = new JSONObject();
1982         try {
1983                 // Need to encode file name in order to properly display it in the web client.
1984                         json.put("name", URLEncoder.encode(file.getName(),"UTF-8")).
1985                                         put("owner", file.getOwner().getUsername()).
1986                                         put("versioned", file.isVersioned()).
1987                                         put("version", oldBody != null ? oldBody.getVersion() : file.getVersion()).
1988                                         put("readForAll", file.isReadForAll()).
1989                                         put("tags", renderJson(file.getTags())).
1990                                         put("path", file.getFolder().getPath()).
1991                                 put("uri", getApiRoot() + file.getURI()).
1992                                         put("deleted", file.isDeleted());
1993                         JSONObject j = new JSONObject();
1994                         j.put("uri", getApiRoot() + file.getFolder().getURI()).
1995                                         put("name", URLEncoder.encode(file.getFolder().getName(),"UTF-8"));
1996                         json.put("folder", j);
1997                         if (oldBody != null)
1998                                 json.put("createdBy", oldBody.getAuditInfo().getCreatedBy().getUsername()).
1999                                                 put("creationDate", oldBody.getAuditInfo().getCreationDate().getTime()).
2000                                                 put("modifiedBy", oldBody.getAuditInfo().getModifiedBy().getUsername()).
2001                                                 put("modificationDate", oldBody.getAuditInfo().getModificationDate().getTime()).
2002                                                 put("content", oldBody.getMimeType()).
2003                                                 put("size", oldBody.getFileSize());
2004                         else
2005                                 json.put("createdBy", file.getAuditInfo().getCreatedBy().getUsername()).
2006                                                 put("creationDate", file.getAuditInfo().getCreationDate().getTime()).
2007                                                 put("modifiedBy", file.getAuditInfo().getModifiedBy().getUsername()).
2008                                                 put("modificationDate", file.getAuditInfo().getModificationDate().getTime()).
2009                                                 put("content", file.getMimeType()).
2010                                                 put("size", file.getFileSize());
2011                 Set<PermissionDTO> perms = getService().getFilePermissions(user.getId(), file.getId());
2012                 json.put("permissions", renderJson(perms));
2013                 } catch (JSONException e) {
2014                         throw new ServletException(e);
2015                 } catch (ObjectNotFoundException e) {
2016                         throw new ServletException(e);
2017                 } catch (RpcException e) {
2018                         throw new ServletException(e);
2019                 } catch (UnsupportedEncodingException e) {
2020                         throw new ServletException(e);
2021                 }
2022
2023         return json.toString();
2024     }
2025
2026         /**
2027          * Return a String with a JSON representation of the
2028          * specified set of permissions.
2029      *
2030          * @param permissions the set of permissions
2031          * @return the JSON-encoded object
2032          * @throws JSONException
2033          * @throws UnsupportedEncodingException
2034          */
2035         private JSONArray renderJson(Set<PermissionDTO> permissions) throws JSONException, UnsupportedEncodingException {
2036                 JSONArray perms = new JSONArray();
2037                 for (PermissionDTO p: permissions) {
2038                         JSONObject permission = new JSONObject();
2039                         permission.put("read", p.hasRead()).put("write", p.hasWrite()).put("modifyACL", p.hasModifyACL());
2040                         if (p.getUser() != null)
2041                                 permission.put("user", p.getUser().getUsername());
2042                         if (p.getGroup() != null) {
2043                                 GroupDTO group = p.getGroup();
2044                                 permission.put("groupUri", getApiRoot() + group.getOwner().getUsername() + PATH_GROUPS + "/" + URLEncoder.encode(group.getName(),"UTF-8"));
2045                                 permission.put("group", URLEncoder.encode(p.getGroup().getName(),"UTF-8"));
2046                         }
2047                         perms.put(permission);
2048                 }
2049                 return perms;
2050         }
2051
2052         /**
2053          * Return a String with a JSON representation of the
2054          * specified collection of tags.
2055      *
2056          * @param tags the collection of tags
2057          * @return the JSON-encoded object
2058          * @throws JSONException
2059          * @throws UnsupportedEncodingException
2060          */
2061         private JSONArray renderJson(Collection<String> tags) throws JSONException, UnsupportedEncodingException {
2062                 JSONArray tagArray = new JSONArray();
2063                 for (String t: tags)
2064                         tagArray.put(URLEncoder.encode(t,"UTF-8"));
2065                 return tagArray;
2066         }
2067
2068         /**
2069          * Retrieves the user who owns the destination namespace, for a
2070          * copy or move request.
2071          *
2072          * @param req the HTTP request
2073          * @return the owner of the namespace
2074          */
2075         protected User getDestinationOwner(HttpServletRequest req) {
2076                 return (User) req.getAttribute(DESTINATION_OWNER_ATTRIBUTE);
2077         }
2078
2079         /**
2080          * A helper inner class for updating the progress status of a file upload.
2081          *
2082          * @author kman
2083          */
2084         public static class StatusProgressListener implements ProgressListener {
2085                 private int percentLogged = 0;
2086                 private long bytesTransferred = 0;
2087
2088                 private long fileSize = -100;
2089
2090                 private Long userId;
2091
2092                 private String filename;
2093
2094                 private ExternalAPI service;
2095
2096                 public StatusProgressListener(ExternalAPI aService) {
2097                         service = aService;
2098                 }
2099
2100                 /**
2101                  * Modify the userId.
2102                  *
2103                  * @param aUserId the userId to set
2104                  */
2105                 public void setUserId(Long aUserId) {
2106                         userId = aUserId;
2107                 }
2108
2109                 /**
2110                  * Modify the filename.
2111                  *
2112                  * @param aFilename the filename to set
2113                  */
2114                 public void setFilename(String aFilename) {
2115                         filename = aFilename;
2116                 }
2117
2118                 public void update(long bytesRead, long contentLength, int items) {
2119                         //monitoring per percent of bytes uploaded
2120                         bytesTransferred = bytesRead;
2121                         if (fileSize != contentLength)
2122                                 fileSize = contentLength;
2123                         int percent = new Long(bytesTransferred * 100 / fileSize).intValue();
2124
2125                         if (percent < 5 || percent % TRACK_PROGRESS_PERCENT == 0 )
2126                                 if (percent != percentLogged){
2127                                         percentLogged = percent;
2128                                         try {
2129                                                 if (userId != null && filename != null)
2130                                                         service.createFileUploadProgress(userId, filename, bytesTransferred, fileSize);
2131                                         } catch (ObjectNotFoundException e) {
2132                                                 // Swallow the exception since it is going to be caught
2133                                                 // by previously called methods
2134                                         }
2135                                 }
2136                 }
2137         }
2138 }