Java tutorial
/* * Copyright 2007, 2008, 2009 Electronic Business Systems Ltd. * * This file is part of GSS. * * GSS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * GSS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GSS. If not, see <http://www.gnu.org/licenses/>. */ package org.gss_project.gss.server.rest; import static org.gss_project.gss.server.configuration.GSSConfigurationFactory.getConfiguration; import org.gss_project.gss.common.exceptions.DuplicateNameException; import org.gss_project.gss.common.exceptions.GSSIOException; import org.gss_project.gss.common.exceptions.InsufficientPermissionsException; import org.gss_project.gss.common.exceptions.ObjectNotFoundException; import org.gss_project.gss.common.exceptions.QuotaExceededException; import org.gss_project.gss.common.exceptions.RpcException; import org.gss_project.gss.server.domain.AuditInfo; import org.gss_project.gss.server.domain.FileBody; import org.gss_project.gss.server.domain.FileHeader; import org.gss_project.gss.server.domain.Folder; import org.gss_project.gss.server.domain.User; import org.gss_project.gss.server.ejb.ExternalAPI; import org.gss_project.gss.server.ejb.TransactionHelper; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.RandomAccessFile; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.URLDecoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Stack; import java.util.StringTokenizer; import java.util.TimeZone; import java.util.Vector; import java.util.concurrent.Callable; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.rmi.PortableRemoteObject; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; /** * The implementation of the WebDAV service. * * @author past */ public class Webdav extends HttpServlet { /** * The request attribute containing the user who owns the requested * namespace. */ protected static final String OWNER_ATTRIBUTE = "owner"; /** * The request attribute containing the user making the request. */ protected static final String USER_ATTRIBUTE = "user"; /** * The logger. */ private static Log logger = LogFactory.getLog(Webdav.class); /** * */ protected static final String METHOD_GET = "GET"; /** * */ protected static final String METHOD_POST = "POST"; /** * */ protected static final String METHOD_PUT = "PUT"; /** * */ protected static final String METHOD_DELETE = "DELETE"; /** * */ protected static final String METHOD_HEAD = "HEAD"; /** * */ private static final String METHOD_OPTIONS = "OPTIONS"; /** * */ private static final String METHOD_PROPFIND = "PROPFIND"; /** * */ private static final String METHOD_PROPPATCH = "PROPPATCH"; /** * */ private static final String METHOD_MKCOL = "MKCOL"; /** * */ private static final String METHOD_COPY = "COPY"; /** * */ private static final String METHOD_MOVE = "MOVE"; /** * */ private static final String METHOD_LOCK = "LOCK"; /** * */ private static final String METHOD_UNLOCK = "UNLOCK"; /** * Default depth is infinite. */ static final int INFINITY = 3; // To limit tree browsing a bit /** * PROPFIND - Specify a property mask. */ private static final int FIND_BY_PROPERTY = 0; /** * PROPFIND - Display all properties. */ private static final int FIND_ALL_PROP = 1; /** * PROPFIND - Return property names. */ private static final int FIND_PROPERTY_NAMES = 2; /** * Default namespace. */ private static final String DEFAULT_NAMESPACE = "DAV:"; /** * Create a new lock. */ private static final int LOCK_CREATION = 0; /** * Refresh lock. */ private static final int LOCK_REFRESH = 1; /** * Default lock timeout value. */ private static final int DEFAULT_TIMEOUT = 3600; /** * Maximum lock timeout. */ private static final int MAX_TIMEOUT = 604800; /** * Size of file transfer buffer in bytes. */ private static final int BUFFER_SIZE = 4096; /** * The output buffer size to use when serving resources. */ protected int output = 2048; /** * The input buffer size to use when serving resources. */ private int input = 2048; /** * MIME multipart separation string */ protected static final String mimeSeparation = "GSS_MIME_BOUNDARY"; /** * Simple date format for the creation date ISO representation (partial). */ private static final SimpleDateFormat creationDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); /** * HTTP date format. */ private static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); /** * Array containing the safe characters set. */ private static URLEncoder urlEncoder; /** * File encoding to be used when reading static files. If none is specified * the platform default is used. */ private String fileEncoding = null; /** * The style sheet for displaying the directory listings. */ private static final String GSS_CSS = "H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} " + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} " + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} " + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} " + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} " + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}" + "A {color : black;}" + "A.name {color : black;}" + "HR {color : #525D76;}"; /** * Secret information used to generate reasonably secure lock ids. */ private String secret = "gss-webdav"; /** * Full range marker. */ protected static ArrayList FULL = new ArrayList(); /** * MD5 message digest provider. */ protected static MessageDigest md5Helper; /** * The MD5 helper object for this class. */ protected static final MD5Encoder md5Encoder = new MD5Encoder(); /** * GMT timezone - all HTTP dates are on GMT */ static { creationDateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); urlEncoder = new URLEncoder(); urlEncoder.addSafeCharacter('-'); urlEncoder.addSafeCharacter('_'); urlEncoder.addSafeCharacter('.'); urlEncoder.addSafeCharacter('*'); urlEncoder.addSafeCharacter('/'); } @Override public void init() throws ServletException { if (getServletConfig().getInitParameter("input") != null) input = Integer.parseInt(getServletConfig().getInitParameter("input")); if (getServletConfig().getInitParameter("output") != null) output = Integer.parseInt(getServletConfig().getInitParameter("output")); fileEncoding = getServletConfig().getInitParameter("fileEncoding"); // Sanity check on the specified buffer sizes if (input < 256) input = 256; if (output < 256) output = 256; if (logger.isDebugEnabled()) logger.debug("Input buffer size=" + input + ", output buffer size=" + output); if (getServletConfig().getInitParameter("secret") != null) secret = getServletConfig().getInitParameter("secret"); // Load the MD5 helper used to calculate signatures. try { md5Helper = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new UnavailableException("No MD5"); } } /** * A helper method that retrieves a reference to the ExternalAPI bean and * stores it for future use. * * @return an ExternalAPI instance * @throws RpcException in case an error occurs */ protected ExternalAPI getService() throws RpcException { try { final Context ctx = new InitialContext(); final Object ref = ctx.lookup(getConfiguration().getString("externalApiPath")); return (ExternalAPI) PortableRemoteObject.narrow(ref, ExternalAPI.class); } catch (final NamingException e) { logger.error("Unable to retrieve the ExternalAPI EJB", e); throw new RpcException("An error occurred while contacting the naming service"); } } private void updateAccounting(final User user, final Date date, final long bandwidthDiff) { try { new TransactionHelper<Void>().tryExecute(new Callable<Void>() { @Override public Void call() throws Exception { getService().updateAccounting(user, date, bandwidthDiff); return null; } }); } catch (RuntimeException e) { throw e; } catch (Exception e) { // updateAccounting() doesn't throw any checked exceptions assert false; } } @Override public void service(final HttpServletRequest request, final HttpServletResponse response) throws IOException, ServletException { String method = request.getMethod(); if (logger.isDebugEnabled()) { String path = request.getPathInfo(); if (path == null) path = request.getServletPath(); if (path == null || path.equals("")) path = "/"; logger.debug("[" + method + "] " + path); } try { User user = null; if (request.getUserPrincipal() != null) { // Let unauthenticated // OPTIONS go through; // all others will be // blocked by // authentication anyway // before we get here. user = getService().findUser(request.getUserPrincipal().getName()); if (user == null) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } } request.setAttribute(USER_ATTRIBUTE, user); request.setAttribute(OWNER_ATTRIBUTE, user); } catch (RpcException e) { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } if (method.equals(METHOD_GET)) doGet(request, response); else if (method.equals(METHOD_POST)) doPost(request, response); else if (method.equals(METHOD_PUT)) doPut(request, response); else if (method.equals(METHOD_DELETE)) doDelete(request, response); else if (method.equals(METHOD_HEAD)) doHead(request, response); else if (method.equals(METHOD_PROPFIND)) doPropfind(request, response); else if (method.equals(METHOD_PROPPATCH)) doProppatch(request, response); else if (method.equals(METHOD_MKCOL)) doMkcol(request, response); else if (method.equals(METHOD_COPY)) doCopy(request, response); else if (method.equals(METHOD_MOVE)) doMove(request, response); else if (method.equals(METHOD_LOCK)) doLock(request, response); else if (method.equals(METHOD_UNLOCK)) doUnlock(request, response); else if (method.equals(METHOD_OPTIONS)) doOptions(request, response); else // DefaultServlet processing for TRACE, etc. super.service(request, response); } @Override protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.addHeader("DAV", "1,2"); StringBuffer methodsAllowed = new StringBuffer(); try { methodsAllowed = determineMethodsAllowed(req); } catch (RpcException e) { resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR); return; } resp.addHeader("Allow", methodsAllowed.toString()); resp.addHeader("MS-Author-Via", "DAV"); } /** * Implement the PROPFIND method. * * @param req the HTTP request * @param resp the HTTP response * @throws ServletException * @throws IOException */ private void doPropfind(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String path = getRelativePath(req); if (path.endsWith("/") && !path.equals("/")) path = path.substring(0, path.length() - 1); if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return; } // Properties which are to be displayed. Vector<String> properties = null; // Propfind depth int depth = INFINITY; // Propfind type int type = FIND_ALL_PROP; String depthStr = req.getHeader("Depth"); if (depthStr == null) depth = INFINITY; else if (depthStr.equals("0")) depth = 0; else if (depthStr.equals("1")) depth = 1; else if (depthStr.equals("infinity")) depth = INFINITY; Node propNode = null; if (req.getInputStream().available() > 0) { DocumentBuilder documentBuilder = getDocumentBuilder(); try { Document document = documentBuilder.parse(new InputSource(req.getInputStream())); // Get the root element of the document Element rootElement = document.getDocumentElement(); NodeList childList = rootElement.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: if (currentNode.getNodeName().endsWith("prop")) { type = FIND_BY_PROPERTY; propNode = currentNode; } if (currentNode.getNodeName().endsWith("propname")) type = FIND_PROPERTY_NAMES; if (currentNode.getNodeName().endsWith("allprop")) type = FIND_ALL_PROP; break; } } } catch (SAXException e) { // Something went wrong - use the defaults. if (logger.isDebugEnabled()) logger.debug(e.getMessage()); } catch (IOException e) { // Something went wrong - use the defaults. if (logger.isDebugEnabled()) logger.debug(e.getMessage()); } } if (type == FIND_BY_PROPERTY) { properties = new Vector<String>(); NodeList childList = propNode.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: String nodeName = currentNode.getNodeName(); String propertyName = null; if (nodeName.indexOf(':') != -1) propertyName = nodeName.substring(nodeName.indexOf(':') + 1); else propertyName = nodeName; // href is a live property which is handled differently properties.addElement(propertyName); break; } } } User user = getUser(req); boolean exists = true; Object object = null; try { object = getService().getResourceAtPath(user.getId(), path, true); } catch (ObjectNotFoundException e) { exists = false; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } if (!exists) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, path); return; } resp.setStatus(WebdavStatus.SC_MULTI_STATUS); resp.setContentType("text/xml; charset=UTF-8"); // Create multistatus object XMLWriter generatedXML = new XMLWriter(resp.getWriter()); generatedXML.writeXMLHeader(); generatedXML.writeElement(null, "D:multistatus" + generateNamespaceDeclarations(), XMLWriter.OPENING); if (depth == 0) parseProperties(req, generatedXML, path, type, properties, object); else { // The stack always contains the object of the current level Stack<String> stack = new Stack<String>(); stack.push(path); // Stack of the objects one level below Stack<String> stackBelow = new Stack<String>(); while (!stack.isEmpty() && depth >= 0) { String currentPath = stack.pop(); try { object = getService().getResourceAtPath(user.getId(), currentPath, true); } catch (ObjectNotFoundException e) { continue; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } parseProperties(req, generatedXML, currentPath, type, properties, object); if (object instanceof Folder && depth > 0) { Folder folderLocal = (Folder) object; // Retrieve the subfolders. List subfolders = folderLocal.getSubfolders(); Iterator iter = subfolders.iterator(); while (iter.hasNext()) { Folder f = (Folder) iter.next(); String newPath = currentPath; if (!newPath.endsWith("/")) newPath += "/"; newPath += f.getName(); stackBelow.push(newPath); } // Retrieve the files. List<FileHeader> files; try { files = getService().getFiles(user.getId(), folderLocal.getId(), true); } catch (ObjectNotFoundException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } catch (InsufficientPermissionsException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, path); return; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } for (FileHeader file : files) { String newPath = currentPath; if (!newPath.endsWith("/")) newPath += "/"; newPath += file.getName(); stackBelow.push(newPath); } } if (stack.isEmpty()) { depth--; stack = stackBelow; stackBelow = new Stack<String>(); } generatedXML.sendData(); } } generatedXML.writeElement(null, "D:multistatus", XMLWriter.CLOSING); generatedXML.sendData(); } /** * PROPPATCH Method. * * @param req the HTTP request * @param resp the HTTP response * @throws IOException if an error occurs while sending the response */ private void doProppatch(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (isLocked(req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); } @Override protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (isLocked(req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } deleteResource(req, resp); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // Serve the requested resource, including the data content try { serveResource(req, resp, true); } catch (ObjectNotFoundException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } catch (InsufficientPermissionsException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); } @Override protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException { if (isLocked(req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } final User user = getUser(req); String path = getRelativePath(req); boolean exists = true; Object resource = null; FileHeader file = null; try { resource = getService().getResourceAtPath(user.getId(), path, true); } catch (ObjectNotFoundException e) { exists = false; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } if (exists) if (resource instanceof FileHeader) file = (FileHeader) resource; else { resp.sendError(HttpServletResponse.SC_CONFLICT); return; } boolean result = true; // Temporary content file used to support partial PUT. File contentFile = null; Range range = parseContentRange(req, resp); InputStream resourceInputStream = null; // Append data specified in ranges to existing content for this // resource - create a temporary file on the local filesystem to // perform this operation. // Assume just one range is specified for now if (range != null) { try { contentFile = executePartialPut(req, range, path); } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } catch (ObjectNotFoundException e) { resp.sendError(HttpServletResponse.SC_CONFLICT); return; } catch (InsufficientPermissionsException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } resourceInputStream = new FileInputStream(contentFile); } else resourceInputStream = req.getInputStream(); try { Object parent = getService().getResourceAtPath(user.getId(), getParentPath(path), true); if (!(parent instanceof Folder)) { resp.sendError(HttpServletResponse.SC_CONFLICT); return; } final Folder folderLocal = (Folder) parent; final String name = getLastElement(path); final String mimeType = getServletContext().getMimeType(name); File uploadedFile = null; try { uploadedFile = getService().uploadFile(resourceInputStream, user.getId()); } catch (IOException ex) { throw new GSSIOException(ex, false); } // FIXME: Add attributes FileHeader fileLocal = null; final FileHeader f = file; final File uf = uploadedFile; if (exists) fileLocal = new TransactionHelper<FileHeader>().tryExecute(new Callable<FileHeader>() { @Override public FileHeader call() throws Exception { return getService().updateFileContents(user.getId(), f.getId(), mimeType, uf.length(), uf.getAbsolutePath()); } }); else fileLocal = new TransactionHelper<FileHeader>().tryExecute(new Callable<FileHeader>() { @Override public FileHeader call() throws Exception { return getService().createFile(user.getId(), folderLocal.getId(), name, mimeType, uf.length(), uf.getAbsolutePath()); } }); updateAccounting(user, new Date(), fileLocal.getCurrentBody().getFileSize()); } catch (ObjectNotFoundException e) { result = false; } catch (InsufficientPermissionsException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } catch (QuotaExceededException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } catch (GSSIOException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } catch (DuplicateNameException e) { resp.sendError(HttpServletResponse.SC_CONFLICT); return; } catch (Exception e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } if (result) { if (exists) resp.setStatus(HttpServletResponse.SC_NO_CONTENT); else resp.setStatus(HttpServletResponse.SC_CREATED); } else resp.sendError(HttpServletResponse.SC_CONFLICT); } @Override protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { // Serve the requested resource, without the data content try { serveResource(req, resp, false); } catch (ObjectNotFoundException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND); return; } catch (InsufficientPermissionsException e) { resp.sendError(HttpServletResponse.SC_FORBIDDEN); return; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } } /** * The UNLOCK method. * * @param req the HTTP request * @param resp the HTTP response * @throws IOException if an error occurs while sending the response */ private void doUnlock(@SuppressWarnings("unused") HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setStatus(WebdavStatus.SC_NO_CONTENT); } /** * The LOCK method. * * @param req the HTTP request * @param resp the HTTP response * @throws IOException if an error occurs while sending the response * @throws ServletException */ private void doLock(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { LockInfo lock = new LockInfo(); // Parsing lock request // Parsing depth header String depthStr = req.getHeader("Depth"); if (depthStr == null) lock.depth = INFINITY; else if (depthStr.equals("0")) lock.depth = 0; else lock.depth = INFINITY; // Parsing timeout header int lockDuration = DEFAULT_TIMEOUT; String lockDurationStr = req.getHeader("Timeout"); if (lockDurationStr == null) lockDuration = DEFAULT_TIMEOUT; else { int commaPos = lockDurationStr.indexOf(","); // If multiple timeouts, just use the first if (commaPos != -1) lockDurationStr = lockDurationStr.substring(0, commaPos); if (lockDurationStr.startsWith("Second-")) lockDuration = new Integer(lockDurationStr.substring(7)).intValue(); else if (lockDurationStr.equalsIgnoreCase("infinity")) lockDuration = MAX_TIMEOUT; else try { lockDuration = new Integer(lockDurationStr).intValue(); } catch (NumberFormatException e) { lockDuration = MAX_TIMEOUT; } if (lockDuration == 0) lockDuration = DEFAULT_TIMEOUT; if (lockDuration > MAX_TIMEOUT) lockDuration = MAX_TIMEOUT; } lock.expiresAt = System.currentTimeMillis() + lockDuration * 1000; int lockRequestType = LOCK_CREATION; Node lockInfoNode = null; DocumentBuilder documentBuilder = getDocumentBuilder(); try { Document document = documentBuilder.parse(new InputSource(req.getInputStream())); // Get the root element of the document Element rootElement = document.getDocumentElement(); lockInfoNode = rootElement; } catch (IOException e) { lockRequestType = LOCK_REFRESH; } catch (SAXException e) { lockRequestType = LOCK_REFRESH; } if (lockInfoNode != null) { // Reading lock information NodeList childList = lockInfoNode.getChildNodes(); StringWriter strWriter = null; DOMWriter domWriter = null; Node lockScopeNode = null; Node lockTypeNode = null; Node lockOwnerNode = null; for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: String nodeName = currentNode.getNodeName(); if (nodeName.endsWith("lockscope")) lockScopeNode = currentNode; if (nodeName.endsWith("locktype")) lockTypeNode = currentNode; if (nodeName.endsWith("owner")) lockOwnerNode = currentNode; break; } } if (lockScopeNode != null) { childList = lockScopeNode.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: String tempScope = currentNode.getNodeName(); if (tempScope.indexOf(':') != -1) lock.scope = tempScope.substring(tempScope.indexOf(':') + 1); else lock.scope = tempScope; break; } } if (lock.scope == null) // Bad request resp.setStatus(WebdavStatus.SC_BAD_REQUEST); } else // Bad request resp.setStatus(WebdavStatus.SC_BAD_REQUEST); if (lockTypeNode != null) { childList = lockTypeNode.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: break; case Node.ELEMENT_NODE: String tempType = currentNode.getNodeName(); if (tempType.indexOf(':') != -1) lock.type = tempType.substring(tempType.indexOf(':') + 1); else lock.type = tempType; break; } } if (lock.type == null) // Bad request resp.setStatus(WebdavStatus.SC_BAD_REQUEST); } else // Bad request resp.setStatus(WebdavStatus.SC_BAD_REQUEST); if (lockOwnerNode != null) { childList = lockOwnerNode.getChildNodes(); for (int i = 0; i < childList.getLength(); i++) { Node currentNode = childList.item(i); switch (currentNode.getNodeType()) { case Node.TEXT_NODE: lock.owner += currentNode.getNodeValue(); break; case Node.ELEMENT_NODE: strWriter = new StringWriter(); domWriter = new DOMWriter(strWriter, true); domWriter.setQualifiedNames(false); domWriter.print(currentNode); lock.owner += strWriter.toString(); break; } } if (lock.owner == null) // Bad request resp.setStatus(WebdavStatus.SC_BAD_REQUEST); } else lock.owner = new String(); } String path = getRelativePath(req); lock.path = path; User user = getUser(req); boolean exists = true; Object object = null; try { object = getService().getResourceAtPath(user.getId(), path, true); } catch (ObjectNotFoundException e) { exists = false; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } if (lockRequestType == LOCK_CREATION) { // Generating lock id String lockTokenStr = req.getServletPath() + "-" + lock.type + "-" + lock.scope + "-" + req.getUserPrincipal() + "-" + lock.depth + "-" + lock.owner + "-" + lock.tokens + "-" + lock.expiresAt + "-" + System.currentTimeMillis() + "-" + secret; String lockToken = md5Encoder.encode(md5Helper.digest(lockTokenStr.getBytes())); if (exists && object instanceof Folder && lock.depth == INFINITY) // Locking a collection (and all its member resources) lock.tokens.addElement(lockToken); else { // Locking a single resource lock.tokens.addElement(lockToken); // Add the Lock-Token header as by RFC 2518 8.10.1 // - only do this for newly created locks resp.addHeader("Lock-Token", "<opaquelocktoken:" + lockToken + ">"); } } if (lockRequestType == LOCK_REFRESH) { } // Set the status, then generate the XML response containing // the lock information. XMLWriter generatedXML = new XMLWriter(); generatedXML.writeXMLHeader(); generatedXML.writeElement(null, "prop" + generateNamespaceDeclarations(), XMLWriter.OPENING); generatedXML.writeElement(null, "lockdiscovery", XMLWriter.OPENING); lock.toXML(generatedXML); generatedXML.writeElement(null, "lockdiscovery", XMLWriter.CLOSING); generatedXML.writeElement(null, "prop", XMLWriter.CLOSING); resp.setStatus(WebdavStatus.SC_OK); resp.setContentType("text/xml; charset=UTF-8"); Writer writer = resp.getWriter(); writer.write(generatedXML.toString()); writer.close(); } /** * The MOVE method. * * @param req the HTTP request * @param resp the HTTP response * @throws IOException if an error occurs while sending the response * @throws ServletException */ private void doMove(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { if (isLocked(req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } String path = getRelativePath(req); if (copyResource(req, resp)) deleteResource(path, req, resp, false); } /** * The COPY method. * * @param req the HTTP request * @param resp the HTTP response * @throws IOException if an error occurs while sending the response * @throws ServletException */ private void doCopy(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { copyResource(req, resp); } /** * The MKCOL method. * * @param req the HTTP request * @param resp the HTTP response * @throws IOException if an error occurs while sending the response * @throws ServletException */ private void doMkcol(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { if (isLocked(req)) { resp.sendError(WebdavStatus.SC_LOCKED); return; } final String path = getRelativePath(req); if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return; } final User user = getUser(req); boolean exists = true; try { getService().getResourceAtPath(user.getId(), path, true); } catch (ObjectNotFoundException e) { exists = false; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } // Can't create a collection if a resource already exists at the given // path. if (exists) { // Get allowed methods. StringBuffer methodsAllowed; try { methodsAllowed = determineMethodsAllowed(req); } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } resp.addHeader("Allow", methodsAllowed.toString()); resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); return; } if (req.getInputStream().available() > 0) { DocumentBuilder documentBuilder = getDocumentBuilder(); try { @SuppressWarnings("unused") Document document = documentBuilder.parse(new InputSource(req.getInputStream())); // TODO : Process this request body resp.sendError(WebdavStatus.SC_NOT_IMPLEMENTED); return; } catch (SAXException saxe) { // Parse error - assume invalid content resp.sendError(WebdavStatus.SC_BAD_REQUEST); return; } } Object parent; try { parent = getService().getResourceAtPath(user.getId(), getParentPath(path), true); } catch (ObjectNotFoundException e) { resp.sendError(WebdavStatus.SC_CONFLICT, WebdavStatus.getStatusText(WebdavStatus.SC_CONFLICT)); return; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } try { if (parent instanceof Folder) { final Folder folderLocal = (Folder) parent; new TransactionHelper<Void>().tryExecute(new Callable<Void>() { @Override public Void call() throws Exception { getService().createFolder(user.getId(), folderLocal.getId(), getLastElement(path)); return null; } }); } else { resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN)); return; } } catch (DuplicateNameException e) { // XXX If the existing name is a folder we should be returning // SC_METHOD_NOT_ALLOWED, or even better, just do the createFolder // without checking first and then deal with the exceptions. resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN)); return; } catch (InsufficientPermissionsException e) { resp.sendError(WebdavStatus.SC_FORBIDDEN, WebdavStatus.getStatusText(WebdavStatus.SC_FORBIDDEN)); return; } catch (ObjectNotFoundException e) { resp.sendError(WebdavStatus.SC_CONFLICT, WebdavStatus.getStatusText(WebdavStatus.SC_CONFLICT)); return; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } catch (Exception e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } resp.setStatus(WebdavStatus.SC_CREATED); } /** * For a provided path, remove the last element and return the rest, that is * the path of the parent folder. * * @param path the specified path * @return the path of the parent folder * @throws ObjectNotFoundException if the provided string contains no path * delimiters */ protected String getParentPath(String path) throws ObjectNotFoundException { int lastDelimiter = path.lastIndexOf('/'); if (lastDelimiter == 0) return "/"; if (lastDelimiter == -1) // No path found. throw new ObjectNotFoundException("There is no parent in the path: " + path); else if (lastDelimiter < path.length() - 1) // Return the part before the delimiter. return path.substring(0, lastDelimiter); else { // Remove the trailing delimiter and then recurse. String strippedTrail = path.substring(0, lastDelimiter); return getParentPath(strippedTrail); } } /** * Get the last element in a path that denotes the file or folder name. * * @param path the provided path * @return the last element in the path */ protected String getLastElement(String path) { int lastDelimiter = path.lastIndexOf('/'); if (lastDelimiter == -1) // No path found. return path; else if (lastDelimiter < path.length() - 1) // Return the part after the delimiter. return path.substring(lastDelimiter + 1); else { // Remove the trailing delimiter and then recurse. String strippedTrail = path.substring(0, lastDelimiter); return getLastElement(strippedTrail); } } /** * Only use the PathInfo for determining the requested path. If the * ServletPath is non-null, it will be because the WebDAV servlet has been * mapped to a URL other than /* to configure editing at different URL than * normal viewing. * * @param request the servlet request we are processing * @return the relative path * @throws UnsupportedEncodingException */ protected String getRelativePath(HttpServletRequest request) { // Remove the servlet path from the request URI. String p = request.getRequestURI(); String servletPath = request.getContextPath() + request.getServletPath(); String result = p.substring(servletPath.length()); try { result = URLDecoder.decode(result, "UTF-8"); } catch (UnsupportedEncodingException e) { } if (result == null || result.equals("")) result = "/"; return result; } /** * Return JAXP document builder instance. * * @return the DocumentBuilder * @throws ServletException */ private DocumentBuilder getDocumentBuilder() throws ServletException { DocumentBuilder documentBuilder = null; DocumentBuilderFactory documentBuilderFactory = null; try { documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); documentBuilderFactory.setExpandEntityReferences(false); documentBuilder = documentBuilderFactory.newDocumentBuilder(); documentBuilder.setEntityResolver(new WebdavResolver(getServletContext())); } catch (ParserConfigurationException e) { throw new ServletException("Error while creating a document builder"); } return documentBuilder; } /** * Generate the namespace declarations. * * @return the namespace declarations */ private String generateNamespaceDeclarations() { return " xmlns:D=\"" + DEFAULT_NAMESPACE + "\""; } /** * Propfind helper method. Dispays the properties of a lock-null resource. * * @param req the HTTP request * @param resources Resources object associated with this context * @param generatedXML XML response to the Propfind request * @param path Path of the current resource * @param type Propfind type * @param propertiesVector If the propfind type is find properties by name, * then this Vector contains those properties */ @SuppressWarnings("unused") private void parseLockNullProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, Vector propertiesVector) { return; } /** * Propfind helper method. * * @param req The servlet request * @param resources Resources object associated with this context * @param generatedXML XML response to the Propfind request * @param path Path of the current resource * @param type Propfind type * @param propertiesVector If the propfind type is find properties by name, * then this Vector contains those properties * @param resource the resource object */ private void parseProperties(HttpServletRequest req, XMLWriter generatedXML, String path, int type, Vector<String> propertiesVector, Object resource) { // Exclude any resource in the /WEB-INF and /META-INF subdirectories // (the "toUpperCase()" avoids problems on Windows systems) if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) return; Folder folderLocal = null; FileHeader fileLocal = null; if (resource instanceof Folder) folderLocal = (Folder) resource; else fileLocal = (FileHeader) resource; // Retrieve the creation date. long creation = 0; if (folderLocal != null) creation = folderLocal.getAuditInfo().getCreationDate().getTime(); else creation = fileLocal.getAuditInfo().getCreationDate().getTime(); // Retrieve the modification date. long modification = 0; if (folderLocal != null) modification = folderLocal.getAuditInfo().getCreationDate().getTime(); else modification = fileLocal.getAuditInfo().getCreationDate().getTime(); generatedXML.writeElement(null, "D:response", XMLWriter.OPENING); String status = new String( "HTTP/1.1 " + WebdavStatus.SC_OK + " " + WebdavStatus.getStatusText(WebdavStatus.SC_OK)); // Generating href element generatedXML.writeElement(null, "D:href", XMLWriter.OPENING); String href = req.getContextPath() + req.getServletPath(); if (href.endsWith("/") && path.startsWith("/")) href += path.substring(1); else href += path; if (folderLocal != null && !href.endsWith("/")) href += "/"; generatedXML.writeText(rewriteUrl(href)); generatedXML.writeElement(null, "D:href", XMLWriter.CLOSING); String resourceName = path; int lastSlash = path.lastIndexOf('/'); if (lastSlash != -1) resourceName = resourceName.substring(lastSlash + 1); if (resourceName.isEmpty()) resourceName = "/"; switch (type) { case FIND_ALL_PROP: generatedXML.writeElement(null, "D:propstat", XMLWriter.OPENING); generatedXML.writeElement(null, "D:prop", XMLWriter.OPENING); generatedXML.writeProperty(null, "D:creationdate", getISOCreationDate(creation)); generatedXML.writeElement(null, "D:displayname", XMLWriter.OPENING); generatedXML.writeData(resourceName); generatedXML.writeElement(null, "D:displayname", XMLWriter.CLOSING); if (fileLocal != null) { generatedXML.writeProperty(null, "D:getlastmodified", FastHttpDateFormat.formatDate(modification, null)); generatedXML.writeProperty(null, "D:getcontentlength", String.valueOf(fileLocal.getCurrentBody().getFileSize())); String contentType = fileLocal.getCurrentBody().getMimeType(); if (contentType != null) generatedXML.writeProperty(null, "D:getcontenttype", contentType); generatedXML.writeProperty(null, "D:getetag", getETag(fileLocal, null)); generatedXML.writeElement(null, "D:resourcetype", XMLWriter.NO_CONTENT); } else { generatedXML.writeElement(null, "D:resourcetype", XMLWriter.OPENING); generatedXML.writeElement(null, "D:collection", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:resourcetype", XMLWriter.CLOSING); } generatedXML.writeProperty(null, "D:source", ""); String supportedLocks = "<D:lockentry>" + "<D:lockscope><D:exclusive/></D:lockscope>" + "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>" + "<D:lockentry>" + "<D:lockscope><D:shared/></D:lockscope>" + "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>"; generatedXML.writeElement(null, "D:supportedlock", XMLWriter.OPENING); generatedXML.writeText(supportedLocks); generatedXML.writeElement(null, "D:supportedlock", XMLWriter.CLOSING); generateLockDiscovery(path, generatedXML); generatedXML.writeElement(null, "D:prop", XMLWriter.CLOSING); generatedXML.writeElement(null, "D:status", XMLWriter.OPENING); generatedXML.writeText(status); generatedXML.writeElement(null, "D:status", XMLWriter.CLOSING); generatedXML.writeElement(null, "D:propstat", XMLWriter.CLOSING); break; case FIND_PROPERTY_NAMES: generatedXML.writeElement(null, "D:propstat", XMLWriter.OPENING); generatedXML.writeElement(null, "D:prop", XMLWriter.OPENING); generatedXML.writeElement(null, "D:creationdate", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:displayname", XMLWriter.NO_CONTENT); if (fileLocal != null) { generatedXML.writeElement(null, "D:getcontentlanguage", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:getcontentlength", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:getcontenttype", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:getetag", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:getlastmodified", XMLWriter.NO_CONTENT); } generatedXML.writeElement(null, "D:resourcetype", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:source", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:lockdiscovery", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:prop", XMLWriter.CLOSING); generatedXML.writeElement(null, "D:status", XMLWriter.OPENING); generatedXML.writeText(status); generatedXML.writeElement(null, "D:status", XMLWriter.CLOSING); generatedXML.writeElement(null, "D:propstat", XMLWriter.CLOSING); break; case FIND_BY_PROPERTY: Vector<String> propertiesNotFound = new Vector<String>(); // Parse the list of properties generatedXML.writeElement(null, "D:propstat", XMLWriter.OPENING); generatedXML.writeElement(null, "D:prop", XMLWriter.OPENING); Enumeration<String> properties = propertiesVector.elements(); while (properties.hasMoreElements()) { String property = properties.nextElement(); if (property.equals("D:creationdate")) generatedXML.writeProperty(null, "D:creationdate", getISOCreationDate(creation)); else if (property.equals("D:displayname")) { generatedXML.writeElement(null, "D:displayname", XMLWriter.OPENING); generatedXML.writeData(resourceName); generatedXML.writeElement(null, "D:displayname", XMLWriter.CLOSING); } else if (property.equals("D:getcontentlanguage")) { if (folderLocal != null) propertiesNotFound.addElement(property); else generatedXML.writeElement(null, "D:getcontentlanguage", XMLWriter.NO_CONTENT); } else if (property.equals("D:getcontentlength")) { if (folderLocal != null) propertiesNotFound.addElement(property); else generatedXML.writeProperty(null, "D:getcontentlength", String.valueOf(fileLocal.getCurrentBody().getFileSize())); } else if (property.equals("D:getcontenttype")) { if (folderLocal != null) propertiesNotFound.addElement(property); else // XXX Once we properly store the MIME type in the // file, // retrieve it from there. generatedXML.writeProperty(null, "D:getcontenttype", getServletContext().getMimeType(fileLocal.getName())); } else if (property.equals("D:getetag")) { if (folderLocal != null) propertiesNotFound.addElement(property); else generatedXML.writeProperty(null, "D:getetag", getETag(fileLocal, null)); } else if (property.equals("D:getlastmodified")) { if (folderLocal != null) propertiesNotFound.addElement(property); else generatedXML.writeProperty(null, "D:getlastmodified", FastHttpDateFormat.formatDate(modification, null)); } else if (property.equals("D:resourcetype")) { if (folderLocal != null) { generatedXML.writeElement(null, "D:resourcetype", XMLWriter.OPENING); generatedXML.writeElement(null, "D:collection", XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:resourcetype", XMLWriter.CLOSING); } else generatedXML.writeElement(null, "D:resourcetype", XMLWriter.NO_CONTENT); } else if (property.equals("D:source")) generatedXML.writeProperty(null, "D:source", ""); else if (property.equals("D:supportedlock")) { supportedLocks = "<D:lockentry>" + "<D:lockscope><D:exclusive/></D:lockscope>" + "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>" + "<D:lockentry>" + "<D:lockscope><D:shared/></D:lockscope>" + "<D:locktype><D:write/></D:locktype>" + "</D:lockentry>"; generatedXML.writeElement(null, "D:supportedlock", XMLWriter.OPENING); generatedXML.writeText(supportedLocks); generatedXML.writeElement(null, "D:supportedlock", XMLWriter.CLOSING); } else if (property.equals("D:lockdiscovery")) { if (!generateLockDiscovery(path, generatedXML)) propertiesNotFound.addElement(property); } else propertiesNotFound.addElement(property); } generatedXML.writeElement(null, "D:prop", XMLWriter.CLOSING); generatedXML.writeElement(null, "D:status", XMLWriter.OPENING); generatedXML.writeText(status); generatedXML.writeElement(null, "D:status", XMLWriter.CLOSING); generatedXML.writeElement(null, "D:propstat", XMLWriter.CLOSING); Enumeration propertiesNotFoundList = propertiesNotFound.elements(); if (propertiesNotFoundList.hasMoreElements()) { status = new String("HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + " " + WebdavStatus.getStatusText(WebdavStatus.SC_NOT_FOUND)); generatedXML.writeElement(null, "D:propstat", XMLWriter.OPENING); generatedXML.writeElement(null, "D:prop", XMLWriter.OPENING); while (propertiesNotFoundList.hasMoreElements()) generatedXML.writeElement(null, (String) propertiesNotFoundList.nextElement(), XMLWriter.NO_CONTENT); generatedXML.writeElement(null, "D:prop", XMLWriter.CLOSING); generatedXML.writeElement(null, "D:status", XMLWriter.OPENING); generatedXML.writeText(status); generatedXML.writeElement(null, "D:status", XMLWriter.CLOSING); generatedXML.writeElement(null, "D:propstat", XMLWriter.CLOSING); } break; } generatedXML.writeElement(null, "D:response", XMLWriter.CLOSING); } /** * Get the ETag associated with a file. * * @param file the FileHeader object for this file * @param oldBody the old version of the file, if requested * @return a string containing the ETag */ protected String getETag(FileHeader file, FileBody oldBody) { if (oldBody == null) return "\"" + file.getCurrentBody().getFileSize() + "-" + file.getAuditInfo().getModificationDate().getTime() + "\""; return "\"" + oldBody.getFileSize() + "-" + oldBody.getAuditInfo().getModificationDate().getTime() + "\""; } /** * URL rewriter. * * @param path Path which has to be rewritten * @return the rewritten URL */ protected String rewriteUrl(String path) { return urlEncoder.encode(path); } /** * Print the lock discovery information associated with a path. * * @param path Path * @param generatedXML XML data to which the locks info will be appended * @return true if at least one lock was displayed */ @SuppressWarnings("unused") private boolean generateLockDiscovery(String path, XMLWriter generatedXML) { return false; } /** * Get creation date in ISO format. * * @param creationDate * @return the formatted date */ private String getISOCreationDate(long creationDate) { String dateValue = null; synchronized (creationDateFormat) { dateValue = creationDateFormat.format(new Date(creationDate)); } StringBuffer creationDateValue = new StringBuffer(dateValue); /* int offset = Calendar.getInstance().getTimeZone().getRawOffset() / 3600000; // FIXME ? if (offset < 0) { creationDateValue.append("-"); offset = -offset; } else if (offset > 0) { creationDateValue.append("+"); } if (offset != 0) { if (offset < 10) creationDateValue.append("0"); creationDateValue.append(offset + ":00"); } else { creationDateValue.append("Z"); } */ return creationDateValue.toString(); } /** * Determines the methods normally allowed for the resource. * * @param req the HTTP request * @return a list of the allowed methods * @throws RpcException if there is an error while communicating with the * backend */ private StringBuffer determineMethodsAllowed(HttpServletRequest req) throws RpcException { StringBuffer methodsAllowed = new StringBuffer(); boolean exists = true; Object object = null; User user = getUser(req); String path = getRelativePath(req); if (user == null && "/".equals(path)) // Special case: OPTIONS request before authentication return new StringBuffer( "OPTIONS, GET, HEAD, POST, DELETE, TRACE, PROPPATCH, COPY, MOVE, LOCK, UNLOCK, PROPFIND, PUT, MKCOL"); try { object = getService().getResourceAtPath(user.getId(), path, true); } catch (ObjectNotFoundException e) { exists = false; } if (!exists) { methodsAllowed.append("OPTIONS, MKCOL, PUT, LOCK"); return methodsAllowed; } methodsAllowed.append("OPTIONS, GET, HEAD, POST, DELETE, TRACE"); methodsAllowed.append(", PROPPATCH, COPY, MOVE, LOCK, UNLOCK"); methodsAllowed.append(", PROPFIND"); if (!(object instanceof Folder)) methodsAllowed.append(", PUT"); return methodsAllowed; } /** * Check to see if a resource is currently write locked. The method will * look at the "If" header to make sure the client has given the appropriate * lock tokens. * * @param req the HTTP request * @return boolean true if the resource is locked (and no appropriate lock * token has been found for at least one of the non-shared locks * which are present on the resource). */ private boolean isLocked(@SuppressWarnings("unused") HttpServletRequest req) { return false; } /** * Check to see if a resource is currently write locked. * * @param path Path of the resource * @param ifHeader "If" HTTP header which was included in the request * @return boolean true if the resource is locked (and no appropriate lock * token has been found for at least one of the non-shared locks * which are present on the resource). */ private boolean isLocked(@SuppressWarnings("unused") String path, @SuppressWarnings("unused") String ifHeader) { return false; } /** * Parse the content-range header. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @return Range * @throws IOException */ protected Range parseContentRange(HttpServletRequest request, HttpServletResponse response) throws IOException { // Retrieving the content-range header (if any is specified String rangeHeader = request.getHeader("Content-Range"); if (rangeHeader == null) return null; // bytes is the only range unit supported if (!rangeHeader.startsWith("bytes")) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return null; } rangeHeader = rangeHeader.substring(6).trim(); int dashPos = rangeHeader.indexOf('-'); int slashPos = rangeHeader.indexOf('/'); if (dashPos == -1) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return null; } if (slashPos == -1) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return null; } Range range = new Range(); try { range.start = Long.parseLong(rangeHeader.substring(0, dashPos)); range.end = Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos)); range.length = Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length())); } catch (NumberFormatException e) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return null; } if (!range.validate()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST); return null; } return range; } /** * Handle a partial PUT. New content specified in request is appended to * existing content in oldRevisionContent (if present). This code does not * support simultaneous partial updates to the same resource. * * @param req * @param range * @param path * @return * @throws IOException * @throws RpcException * @throws InsufficientPermissionsException * @throws ObjectNotFoundException */ protected File executePartialPut(HttpServletRequest req, Range range, String path) throws IOException, RpcException, ObjectNotFoundException, InsufficientPermissionsException { // Append data specified in ranges to existing content for this // resource - create a temporary file on the local file system to // perform this operation. File tempDir = (File) getServletContext().getAttribute("javax.servlet.context.tempdir"); // Convert all '/' characters to '.' in resourcePath String convertedResourcePath = path.replace('/', '.'); File contentFile = new File(tempDir, convertedResourcePath); if (contentFile.createNewFile()) // Clean up contentFile when Tomcat is terminated. contentFile.deleteOnExit(); RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw"); User user = getUser(req); User owner = getOwner(req); FileHeader oldResource = null; try { Object obj = getService().getResourceAtPath(owner.getId(), path, true); if (obj instanceof FileHeader) oldResource = (FileHeader) obj; } catch (ObjectNotFoundException e) { // Do nothing. } // Copy data in oldRevisionContent to contentFile if (oldResource != null) { InputStream contents = getService().getFileContents(user.getId(), oldResource.getId()); BufferedInputStream bufOldRevStream = new BufferedInputStream(contents, BUFFER_SIZE); int numBytesRead; byte[] copyBuffer = new byte[BUFFER_SIZE]; while ((numBytesRead = bufOldRevStream.read(copyBuffer)) != -1) randAccessContentFile.write(copyBuffer, 0, numBytesRead); bufOldRevStream.close(); } randAccessContentFile.setLength(range.length); // Append data in request input stream to contentFile randAccessContentFile.seek(range.start); int numBytesRead; byte[] transferBuffer = new byte[BUFFER_SIZE]; BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE); while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) randAccessContentFile.write(transferBuffer, 0, numBytesRead); randAccessContentFile.close(); requestBufInStream.close(); return contentFile; } /** * Serve the specified resource, optionally including the data content. * * @param req The servlet request we are processing * @param resp The servlet response we are creating * @param content Should the content be included? * @exception IOException if an input/output error occurs * @exception ServletException if a servlet-specified error occurs * @throws RpcException * @throws InsufficientPermissionsException * @throws ObjectNotFoundException */ protected void serveResource(HttpServletRequest req, HttpServletResponse resp, boolean content) throws IOException, ServletException, ObjectNotFoundException, InsufficientPermissionsException, RpcException { // Identify the requested resource path String path = getRelativePath(req); if (logger.isDebugEnabled()) if (content) logger.debug("Serving resource '" + path + "' headers and data"); else logger.debug("Serving resource '" + path + "' headers only"); User user = getUser(req); boolean exists = true; Object resource = null; FileHeader file = null; Folder folder = null; try { resource = getService().getResourceAtPath(user.getId(), path, true); } catch (ObjectNotFoundException e) { exists = false; } catch (RpcException e) { resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, path); return; } if (!exists) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI()); return; } if (resource instanceof Folder) folder = (Folder) resource; else file = (FileHeader) resource; // If the resource is not a collection, and the resource path // ends with "/" or "\", return NOT FOUND if (folder == null) if (path.endsWith("/") || path.endsWith("\\")) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, req.getRequestURI()); return; } // Check if the conditions specified in the optional If headers are // satisfied. if (folder == null) // Checking If headers if (!checkIfHeaders(req, resp, file, null)) return; // Find content type. String contentType = null; if (file != null) { contentType = file.getCurrentBody().getMimeType(); if (contentType == null) { contentType = getServletContext().getMimeType(file.getName()); file.getCurrentBody().setMimeType(contentType); } } else contentType = "text/html;charset=UTF-8"; ArrayList ranges = null; long contentLength = -1L; if (file != null) { // Accept ranges header resp.setHeader("Accept-Ranges", "bytes"); // Parse range specifier ranges = parseRange(req, resp, file, null); // ETag header resp.setHeader("ETag", getETag(file, null)); // Last-Modified header resp.setHeader("Last-Modified", getLastModifiedHttp(file.getAuditInfo())); // Get content length contentLength = file.getCurrentBody().getFileSize(); // Special case for zero length files, which would cause a // (silent) ISE when setting the output buffer size if (contentLength == 0L) content = false; } ServletOutputStream ostream = null; PrintWriter writer = null; if (content) try { ostream = resp.getOutputStream(); } catch (IllegalStateException e) { // If it fails, we try to get a Writer instead if we're // trying to serve a text file if (contentType == null || contentType.startsWith("text") || contentType.endsWith("xml")) writer = resp.getWriter(); else throw e; } if (folder != null || (ranges == null || ranges.isEmpty()) && req.getHeader("Range") == null || ranges == FULL) { // Set the appropriate output headers if (contentType != null) { if (logger.isDebugEnabled()) logger.debug("DefaultServlet.serveFile: contentType='" + contentType + "'"); resp.setContentType(contentType); } if (file != null && contentLength >= 0) { if (logger.isDebugEnabled()) logger.debug("DefaultServlet.serveFile: contentLength=" + contentLength); if (contentLength < Integer.MAX_VALUE) resp.setContentLength((int) contentLength); else // Set the content-length as String to be able to use a long resp.setHeader("content-length", "" + contentLength); } InputStream renderResult = null; if (folder != null) if (content) // Serve the directory browser renderResult = renderHtml(req.getContextPath(), path, folder, req); // Copy the input stream to our output stream (if requested) if (content) { try { resp.setBufferSize(output); } catch (IllegalStateException e) { // Silent catch } if (ostream != null) copy(file, renderResult, ostream, req, null); else copy(file, renderResult, writer, req, null); updateAccounting(user, new Date(), contentLength); } } else { if (ranges == null || ranges.isEmpty()) return; // Partial content response. resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); if (ranges.size() == 1) { Range range = (Range) ranges.get(0); resp.addHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length); long length = range.end - range.start + 1; if (length < Integer.MAX_VALUE) resp.setContentLength((int) length); else // Set the content-length as String to be able to use a long resp.setHeader("content-length", "" + length); if (contentType != null) { if (logger.isDebugEnabled()) logger.debug("DefaultServlet.serveFile: contentType='" + contentType + "'"); resp.setContentType(contentType); } if (content) { try { resp.setBufferSize(output); } catch (IllegalStateException e) { // Silent catch } if (ostream != null) copy(file, ostream, range, req, null); else copy(file, writer, range, req, null); updateAccounting(user, new Date(), contentLength); } } else { resp.setContentType("multipart/byteranges; boundary=" + mimeSeparation); if (content) { try { resp.setBufferSize(output); } catch (IllegalStateException e) { // Silent catch } if (ostream != null) copy(file, ostream, ranges.iterator(), contentType, req, null); else copy(file, writer, ranges.iterator(), contentType, req, null); } } } } /** * Retrieve the last modified date of a resource in HTTP format. * * @param auditInfo the audit info for the specified resource * @return the last modified date in HTTP format */ protected String getLastModifiedHttp(AuditInfo auditInfo) { Date modifiedDate = auditInfo.getModificationDate(); if (modifiedDate == null) modifiedDate = auditInfo.getCreationDate(); if (modifiedDate == null) modifiedDate = new Date(); String lastModifiedHttp = null; synchronized (format) { lastModifiedHttp = format.format(modifiedDate); } return lastModifiedHttp; } /** * Parse the range header. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param file * @param oldBody the old version of the file, if requested * @return Vector of ranges * @throws IOException */ protected ArrayList parseRange(HttpServletRequest request, HttpServletResponse response, FileHeader file, FileBody oldBody) throws IOException { // Checking If-Range String headerValue = request.getHeader("If-Range"); if (headerValue != null) { long headerValueTime = -1L; try { headerValueTime = request.getDateHeader("If-Range"); } catch (IllegalArgumentException e) { // Do nothing. } String eTag = getETag(file, oldBody); long lastModified = oldBody == null ? file.getAuditInfo().getModificationDate().getTime() : oldBody.getAuditInfo().getModificationDate().getTime(); if (headerValueTime == -1L) { // If the ETag the client gave does not match the entity // etag, then the entire entity is returned. if (!eTag.equals(headerValue.trim())) return FULL; } else // If the timestamp of the entity the client got is older than // the last modification date of the entity, the entire entity // is returned. if (lastModified > headerValueTime + 1000) return FULL; } long fileLength = oldBody == null ? file.getCurrentBody().getFileSize() : oldBody.getFileSize(); if (fileLength == 0) return null; // Retrieving the range header (if any is specified). String rangeHeader = request.getHeader("Range"); if (rangeHeader == null) return null; // bytes is the only range unit supported (and I don't see the point // of adding new ones). if (!rangeHeader.startsWith("bytes")) { response.addHeader("Content-Range", "bytes */" + fileLength); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return null; } rangeHeader = rangeHeader.substring(6); // Vector that will contain all the ranges which are successfully // parsed. ArrayList<Range> result = new ArrayList<Range>(); StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ","); // Parsing the range list while (commaTokenizer.hasMoreTokens()) { String rangeDefinition = commaTokenizer.nextToken().trim(); Range currentRange = new Range(); currentRange.length = fileLength; int dashPos = rangeDefinition.indexOf('-'); if (dashPos == -1) { response.addHeader("Content-Range", "bytes */" + fileLength); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return null; } if (dashPos == 0) try { long offset = Long.parseLong(rangeDefinition); currentRange.start = fileLength + offset; currentRange.end = fileLength - 1; } catch (NumberFormatException e) { response.addHeader("Content-Range", "bytes */" + fileLength); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return null; } else try { currentRange.start = Long.parseLong(rangeDefinition.substring(0, dashPos)); if (dashPos < rangeDefinition.length() - 1) currentRange.end = Long .parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length())); else currentRange.end = fileLength - 1; } catch (NumberFormatException e) { response.addHeader("Content-Range", "bytes */" + fileLength); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return null; } if (!currentRange.validate()) { response.addHeader("Content-Range", "bytes */" + fileLength); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return null; } result.add(currentRange); } return result; } /** * Check if the conditions specified in the optional If headers are * satisfied. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param file the file resource against which the checks will be made * @param oldBody the old version of the file, if requested * @return boolean true if the resource meets all the specified conditions, * and false if any of the conditions is not satisfied, in which * case request processing is stopped * @throws IOException */ protected boolean checkIfHeaders(HttpServletRequest request, HttpServletResponse response, FileHeader file, FileBody oldBody) throws IOException { // TODO : Checking the WebDAV If header return checkIfMatch(request, response, file, oldBody) && checkIfModifiedSince(request, response, file, oldBody) && checkIfNoneMatch(request, response, file, oldBody) && checkIfUnmodifiedSince(request, response, file, oldBody); } /** * Check if the if-match condition is satisfied. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param file the file object * @param oldBody the old version of the file, if requested * @return boolean true if the resource meets the specified condition, and * false if the condition is not satisfied, in which case request * processing is stopped * @throws IOException */ private boolean checkIfMatch(HttpServletRequest request, HttpServletResponse response, FileHeader file, FileBody oldBody) throws IOException { String eTag = getETag(file, oldBody); String headerValue = request.getHeader("If-Match"); if (headerValue != null) if (headerValue.indexOf('*') == -1) { StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ","); boolean conditionSatisfied = false; while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) { String currentToken = commaTokenizer.nextToken(); if (currentToken.trim().equals(eTag)) conditionSatisfied = true; } // If none of the given ETags match, 412 Precodition failed is // sent back. if (!conditionSatisfied) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return false; } } return true; } /** * Check if the if-modified-since condition is satisfied. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param file the file object * @param oldBody the old version of the file, if requested * @return boolean true if the resource meets the specified condition, and * false if the condition is not satisfied, in which case request * processing is stopped */ private boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response, FileHeader file, FileBody oldBody) { try { long headerValue = request.getDateHeader("If-Modified-Since"); long lastModified = oldBody == null ? file.getAuditInfo().getModificationDate().getTime() : oldBody.getAuditInfo().getModificationDate().getTime(); if (headerValue != -1) // If an If-None-Match header has been specified, if modified // since is ignored. if (request.getHeader("If-None-Match") == null && lastModified < headerValue + 1000) { // The entity has not been modified since the date // specified by the client. This is not an error case. response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.setHeader("ETag", getETag(file, oldBody)); return false; } } catch (IllegalArgumentException illegalArgument) { return true; } return true; } /** * Check if the if-none-match condition is satisfied. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param file the file object * @param oldBody the old version of the file, if requested * @return boolean true if the resource meets the specified condition, and * false if the condition is not satisfied, in which case request * processing is stopped * @throws IOException */ private boolean checkIfNoneMatch(HttpServletRequest request, HttpServletResponse response, FileHeader file, FileBody oldBody) throws IOException { String eTag = getETag(file, oldBody); String headerValue = request.getHeader("If-None-Match"); if (headerValue != null) { boolean conditionSatisfied = false; if (!headerValue.equals("*")) { StringTokenizer commaTokenizer = new StringTokenizer(headerValue, ","); while (!conditionSatisfied && commaTokenizer.hasMoreTokens()) { String currentToken = commaTokenizer.nextToken(); if (currentToken.trim().equals(eTag)) conditionSatisfied = true; } } else conditionSatisfied = true; if (conditionSatisfied) { // For GET and HEAD, we should respond with 304 Not Modified. // For every other method, 412 Precondition Failed is sent // back. if ("GET".equals(request.getMethod()) || "HEAD".equals(request.getMethod())) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.setHeader("ETag", getETag(file, oldBody)); return false; } response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return false; } } return true; } /** * Check if the if-unmodified-since condition is satisfied. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param file the file object * @param oldBody the old version of the file, if requested * @return boolean true if the resource meets the specified condition, and * false if the condition is not satisfied, in which case request * processing is stopped * @throws IOException */ private boolean checkIfUnmodifiedSince(HttpServletRequest request, HttpServletResponse response, FileHeader file, FileBody oldBody) throws IOException { try { long lastModified = oldBody == null ? file.getAuditInfo().getModificationDate().getTime() : oldBody.getAuditInfo().getModificationDate().getTime(); long headerValue = request.getDateHeader("If-Unmodified-Since"); if (headerValue != -1) if (lastModified >= headerValue + 1000) { // The entity has not been modified since the date // specified by the client. This is not an error case. response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return false; } } catch (IllegalArgumentException illegalArgument) { return true; } return true; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param file the file resource * @param is * @param ostream The output stream to write to * @param req the HTTP request * @param oldBody the old version of the file, if requested * @exception IOException if an input/output error occurs * @throws RpcException * @throws InsufficientPermissionsException * @throws ObjectNotFoundException */ protected void copy(FileHeader file, InputStream is, ServletOutputStream ostream, HttpServletRequest req, FileBody oldBody) throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException { IOException exception = null; InputStream resourceInputStream = null; User user = getUser(req); if (user == null) throw new ObjectNotFoundException("No user or owner specified"); if (file != null) resourceInputStream = oldBody == null ? getService().getFileContents(user.getId(), file.getId()) : getService().getFileContents(user.getId(), file.getId(), oldBody.getId()); else resourceInputStream = is; InputStream istream = new BufferedInputStream(resourceInputStream, input); // Copy the input stream to the output stream exception = copyRange(istream, ostream); // Clean up the input stream istream.close(); // Rethrow any exception that has occurred if (exception != null) throw exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param istream The input stream to read from * @param ostream The output stream to write to * @return Exception which occurred during processing */ private IOException copyRange(InputStream istream, ServletOutputStream ostream) { // Copy the input stream to the output stream IOException exception = null; byte buffer[] = new byte[input]; int len = buffer.length; while (true) try { len = istream.read(buffer); if (len == -1) break; ostream.write(buffer, 0, len); } catch (IOException e) { exception = e; len = -1; break; } return exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param file * @param is * @param resourceInfo The resource info * @param writer The writer to write to * @param req the HTTP request * @param oldBody the old version of the file, if requested * @exception IOException if an input/output error occurs * @throws RpcException * @throws InsufficientPermissionsException * @throws ObjectNotFoundException */ protected void copy(FileHeader file, InputStream is, PrintWriter writer, HttpServletRequest req, FileBody oldBody) throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException { IOException exception = null; User user = getUser(req); InputStream resourceInputStream = null; if (file != null) resourceInputStream = oldBody == null ? getService().getFileContents(user.getId(), file.getId()) : getService().getFileContents(user.getId(), file.getId(), oldBody.getId()); else resourceInputStream = is; Reader reader; if (fileEncoding == null) reader = new InputStreamReader(resourceInputStream); else reader = new InputStreamReader(resourceInputStream, fileEncoding); // Copy the input stream to the output stream exception = copyRange(reader, writer); // Clean up the reader reader.close(); // Rethrow any exception that has occurred if (exception != null) throw exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param reader The reader to read from * @param writer The writer to write to * @return Exception which occurred during processing */ private IOException copyRange(Reader reader, PrintWriter writer) { // Copy the input stream to the output stream IOException exception = null; char buffer[] = new char[input]; int len = buffer.length; while (true) try { len = reader.read(buffer); if (len == -1) break; writer.write(buffer, 0, len); } catch (IOException e) { exception = e; len = -1; break; } return exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param file * @param writer The writer to write to * @param ranges Enumeration of the ranges the client wanted to retrieve * @param contentType Content type of the resource * @param req the HTTP request * @param oldBody the old version of the file, if requested * @exception IOException if an input/output error occurs * @throws RpcException * @throws InsufficientPermissionsException * @throws ObjectNotFoundException */ protected void copy(FileHeader file, PrintWriter writer, Iterator ranges, String contentType, HttpServletRequest req, FileBody oldBody) throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException { User user = getUser(req); IOException exception = null; while (exception == null && ranges.hasNext()) { InputStream resourceInputStream = oldBody == null ? getService().getFileContents(user.getId(), file.getId()) : getService().getFileContents(user.getId(), file.getId(), oldBody.getId()); Reader reader; if (fileEncoding == null) reader = new InputStreamReader(resourceInputStream); else reader = new InputStreamReader(resourceInputStream, fileEncoding); Range currentRange = (Range) ranges.next(); // Writing MIME header. writer.println(); writer.println("--" + mimeSeparation); if (contentType != null) writer.println("Content-Type: " + contentType); writer.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" + currentRange.length); writer.println(); // Printing content exception = copyRange(reader, writer, currentRange.start, currentRange.end); reader.close(); } writer.println(); writer.print("--" + mimeSeparation + "--"); // Rethrow any exception that has occurred if (exception != null) throw exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param istream The input stream to read from * @param ostream The output stream to write to * @param start Start of the range which will be copied * @param end End of the range which will be copied * @return Exception which occurred during processing */ private IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) { if (logger.isDebugEnabled()) logger.debug("Serving bytes:" + start + "-" + end); try { istream.skip(start); } catch (IOException e) { return e; } IOException exception = null; long bytesToRead = end - start + 1; byte buffer[] = new byte[input]; int len = buffer.length; while (bytesToRead > 0 && len >= buffer.length) { try { len = istream.read(buffer); if (bytesToRead >= len) { ostream.write(buffer, 0, len); bytesToRead -= len; } else { ostream.write(buffer, 0, (int) bytesToRead); bytesToRead = 0; } } catch (IOException e) { exception = e; len = -1; } if (len < buffer.length) break; } return exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param reader The reader to read from * @param writer The writer to write to * @param start Start of the range which will be copied * @param end End of the range which will be copied * @return Exception which occurred during processing */ private IOException copyRange(Reader reader, PrintWriter writer, long start, long end) { try { reader.skip(start); } catch (IOException e) { return e; } IOException exception = null; long bytesToRead = end - start + 1; char buffer[] = new char[input]; int len = buffer.length; while (bytesToRead > 0 && len >= buffer.length) { try { len = reader.read(buffer); if (bytesToRead >= len) { writer.write(buffer, 0, len); bytesToRead -= len; } else { writer.write(buffer, 0, (int) bytesToRead); bytesToRead = 0; } } catch (IOException e) { exception = e; len = -1; } if (len < buffer.length) break; } return exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param file * @param ostream The output stream to write to * @param range Range the client wanted to retrieve * @param req the HTTP request * @param oldBody the old version of the file, if requested * @exception IOException if an input/output error occurs * @throws RpcException * @throws InsufficientPermissionsException * @throws ObjectNotFoundException */ protected void copy(FileHeader file, ServletOutputStream ostream, Range range, HttpServletRequest req, FileBody oldBody) throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException { IOException exception = null; User user = getUser(req); InputStream resourceInputStream = oldBody == null ? getService().getFileContents(user.getId(), file.getId()) : getService().getFileContents(user.getId(), file.getId(), oldBody.getId()); InputStream istream = new BufferedInputStream(resourceInputStream, input); exception = copyRange(istream, ostream, range.start, range.end); // Clean up the input stream istream.close(); // Rethrow any exception that has occurred if (exception != null) throw exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param file * @param writer The writer to write to * @param range Range the client wanted to retrieve * @param req the HTTP request * @param oldBody the old version of the file, if requested * @exception IOException if an input/output error occurs * @throws RpcException * @throws InsufficientPermissionsException * @throws ObjectNotFoundException */ protected void copy(FileHeader file, PrintWriter writer, Range range, HttpServletRequest req, FileBody oldBody) throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException { IOException exception = null; User user = getUser(req); InputStream resourceInputStream = oldBody == null ? getService().getFileContents(user.getId(), file.getId()) : getService().getFileContents(user.getId(), file.getId(), oldBody.getId()); Reader reader; if (fileEncoding == null) reader = new InputStreamReader(resourceInputStream); else reader = new InputStreamReader(resourceInputStream, fileEncoding); exception = copyRange(reader, writer, range.start, range.end); // Clean up the input stream reader.close(); // Rethrow any exception that has occurred if (exception != null) throw exception; } /** * Copy the contents of the specified input stream to the specified output * stream, and ensure that both streams are closed before returning (even in * the face of an exception). * * @param file * @param ostream The output stream to write to * @param ranges Enumeration of the ranges the client wanted to retrieve * @param contentType Content type of the resource * @param req the HTTP request * @param oldBody the old version of the file, if requested * @exception IOException if an input/output error occurs * @throws RpcException * @throws InsufficientPermissionsException * @throws ObjectNotFoundException */ protected void copy(FileHeader file, ServletOutputStream ostream, Iterator ranges, String contentType, HttpServletRequest req, FileBody oldBody) throws IOException, ObjectNotFoundException, InsufficientPermissionsException, RpcException { IOException exception = null; User user = getUser(req); while (exception == null && ranges.hasNext()) { InputStream resourceInputStream = oldBody == null ? getService().getFileContents(user.getId(), file.getId()) : getService().getFileContents(user.getId(), file.getId(), oldBody.getId()); InputStream istream = new BufferedInputStream(resourceInputStream, input); Range currentRange = (Range) ranges.next(); // Writing MIME header. ostream.println(); ostream.println("--" + mimeSeparation); if (contentType != null) ostream.println("Content-Type: " + contentType); ostream.println("Content-Range: bytes " + currentRange.start + "-" + currentRange.end + "/" + currentRange.length); ostream.println(); // Printing content exception = copyRange(istream, ostream, currentRange.start, currentRange.end); istream.close(); } ostream.println(); ostream.print("--" + mimeSeparation + "--"); // Rethrow any exception that has occurred if (exception != null) throw exception; } /** * Return an InputStream to an HTML representation of the contents of this * directory. * * @param contextPath Context path to which our internal paths are relative * @param path the requested path to the resource * @param folder the specified directory * @param req the HTTP request * @return an input stream with the rendered contents * @throws IOException * @throws ServletException */ private InputStream renderHtml(String contextPath, String path, Folder folder, HttpServletRequest req) throws IOException, ServletException { String name = folder.getName(); // Prepare a writer to a buffered area ByteArrayOutputStream stream = new ByteArrayOutputStream(); OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8"); PrintWriter writer = new PrintWriter(osWriter); StringBuffer sb = new StringBuffer(); // rewriteUrl(contextPath) is expensive. cache result for later reuse String rewrittenContextPath = rewriteUrl(contextPath); // Render the page header sb.append("<html>\r\n"); sb.append("<head>\r\n"); sb.append("<title>"); sb.append("Index of " + name); sb.append("</title>\r\n"); sb.append("<STYLE><!--"); sb.append(GSS_CSS); sb.append("--></STYLE> "); sb.append("</head>\r\n"); sb.append("<body>"); sb.append("<h1>"); sb.append("Index of " + name); // Render the link to our parent (if required) String parentDirectory = path; if (parentDirectory.endsWith("/")) parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1); int slash = parentDirectory.lastIndexOf('/'); if (slash >= 0) { String parent = path.substring(0, slash); sb.append(" - <a href=\""); sb.append(rewrittenContextPath); if (parent.equals("")) parent = "/"; sb.append(rewriteUrl(parent)); if (!parent.endsWith("/")) sb.append("/"); sb.append("\">"); sb.append("<b>"); sb.append("Up To " + parent); sb.append("</b>"); sb.append("</a>"); } sb.append("</h1>"); sb.append("<HR size=\"1\" noshade=\"noshade\">"); sb.append("<table width=\"100%\" cellspacing=\"0\"" + " cellpadding=\"5\" align=\"center\">\r\n"); // Render the column headings sb.append("<tr>\r\n"); sb.append("<td align=\"left\"><font size=\"+1\"><strong>"); sb.append("Name"); sb.append("</strong></font></td>\r\n"); sb.append("<td align=\"center\"><font size=\"+1\"><strong>"); sb.append("Size"); sb.append("</strong></font></td>\r\n"); sb.append("<td align=\"right\"><font size=\"+1\"><strong>"); sb.append("Last modified"); sb.append("</strong></font></td>\r\n"); sb.append("</tr>"); // Render the directory entries within this directory boolean shade = false; Iterator iter = folder.getSubfolders().iterator(); while (iter.hasNext()) { Folder subf = (Folder) iter.next(); String resourceName = subf.getName(); if (resourceName.equalsIgnoreCase("WEB-INF") || resourceName.equalsIgnoreCase("META-INF")) continue; sb.append("<tr"); if (shade) sb.append(" bgcolor=\"#eeeeee\""); sb.append(">\r\n"); shade = !shade; sb.append("<td align=\"left\"> \r\n"); sb.append("<a href=\""); sb.append(rewrittenContextPath); sb.append(rewriteUrl(path + resourceName)); sb.append("/"); sb.append("\"><tt>"); sb.append(RequestUtil.filter(resourceName)); sb.append("/"); sb.append("</tt></a></td>\r\n"); sb.append("<td align=\"right\"><tt>"); sb.append(" "); sb.append("</tt></td>\r\n"); sb.append("<td align=\"right\"><tt>"); sb.append(getLastModifiedHttp(folder.getAuditInfo())); sb.append("</tt></td>\r\n"); sb.append("</tr>\r\n"); } List<FileHeader> files; try { User user = getUser(req); files = getService().getFiles(user.getId(), folder.getId(), true); } catch (ObjectNotFoundException e) { throw new ServletException(e.getMessage()); } catch (InsufficientPermissionsException e) { throw new ServletException(e.getMessage()); } catch (RpcException e) { throw new ServletException(e.getMessage()); } for (FileHeader fileLocal : files) { String resourceName = fileLocal.getName(); if (resourceName.equalsIgnoreCase("WEB-INF") || resourceName.equalsIgnoreCase("META-INF")) continue; sb.append("<tr"); if (shade) sb.append(" bgcolor=\"#eeeeee\""); sb.append(">\r\n"); shade = !shade; sb.append("<td align=\"left\"> \r\n"); sb.append("<a href=\""); sb.append(rewrittenContextPath); sb.append(rewriteUrl(path + resourceName)); sb.append("\"><tt>"); sb.append(RequestUtil.filter(resourceName)); sb.append("</tt></a></td>\r\n"); sb.append("<td align=\"right\"><tt>"); sb.append(renderSize(fileLocal.getCurrentBody().getFileSize())); sb.append("</tt></td>\r\n"); sb.append("<td align=\"right\"><tt>"); sb.append(getLastModifiedHttp(fileLocal.getAuditInfo())); sb.append("</tt></td>\r\n"); sb.append("</tr>\r\n"); } // Render the page footer sb.append("</table>\r\n"); sb.append("<HR size=\"1\" noshade=\"noshade\">"); sb.append("<h3>").append(getServletContext().getServerInfo()).append("</h3>"); sb.append("</body>\r\n"); sb.append("</html>\r\n"); // Return an input stream to the underlying bytes writer.write(sb.toString()); writer.flush(); return new ByteArrayInputStream(stream.toByteArray()); } /** * Render the specified file size (in bytes). * * @param size File size (in bytes) * @return the size as a string */ protected String renderSize(long size) { long leftSide = size / 1024; long rightSide = size % 1024 / 103; // Makes 1 digit if (leftSide == 0 && rightSide == 0 && size > 0) rightSide = 1; return "" + leftSide + "." + rightSide + " kb"; } /** * Copy a resource. * * @param req Servlet request * @param resp Servlet response * @return boolean true if the copy is successful * @throws IOException */ private boolean copyResource(HttpServletRequest req, HttpServletResponse resp) throws IOException { // Parsing destination header String destinationPath = req.getHeader("Destination"); if (destinationPath == null) { resp.sendError(WebdavStatus.SC_BAD_REQUEST); return false; } // Remove url encoding from destination destinationPath = RequestUtil.URLDecode(destinationPath, "UTF8"); int protocolIndex = destinationPath.indexOf("://"); if (protocolIndex >= 0) { // if the Destination URL contains the protocol, we can safely // trim everything upto the first "/" character after "://" int firstSeparator = destinationPath.indexOf("/", protocolIndex + 4); if (firstSeparator < 0) destinationPath = "/"; else destinationPath = destinationPath.substring(firstSeparator); } else { String hostName = req.getServerName(); if (hostName != null && destinationPath.startsWith(hostName)) destinationPath = destinationPath.substring(hostName.length()); int portIndex = destinationPath.indexOf(":"); if (portIndex >= 0) destinationPath = destinationPath.substring(portIndex); if (destinationPath.startsWith(":")) { int firstSeparator = destinationPath.indexOf("/"); if (firstSeparator < 0) destinationPath = "/"; else destinationPath = destinationPath.substring(firstSeparator); } } // Normalize destination path (remove '.' and '..') destinationPath = RequestUtil.normalize(destinationPath); String contextPath = req.getContextPath(); if (contextPath != null && destinationPath.startsWith(contextPath)) destinationPath = destinationPath.substring(contextPath.length()); String pathInfo = req.getPathInfo(); if (pathInfo != null) { String servletPath = req.getServletPath(); if (servletPath != null && destinationPath.startsWith(servletPath)) destinationPath = destinationPath.substring(servletPath.length()); } if (logger.isDebugEnabled()) logger.debug("Dest path :" + destinationPath); if (destinationPath.toUpperCase().startsWith("/WEB-INF") || destinationPath.toUpperCase().startsWith("/META-INF")) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } String path = getRelativePath(req); if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } if (destinationPath.equals(path)) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } // Parsing overwrite header boolean overwrite = true; String overwriteHeader = req.getHeader("Overwrite"); if (overwriteHeader != null) if (overwriteHeader.equalsIgnoreCase("T")) overwrite = true; else overwrite = false; User user = getUser(req); // Overwriting the destination boolean exists = true; try { getService().getResourceAtPath(user.getId(), destinationPath, true); } catch (ObjectNotFoundException e) { exists = false; } catch (RpcException e) { resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR); return false; } if (overwrite) { // Delete destination resource, if it exists if (exists) { if (!deleteResource(destinationPath, req, resp, true)) return false; } else resp.setStatus(WebdavStatus.SC_CREATED); } else // If the destination exists, then it's a conflict if (exists) { resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); return false; } else resp.setStatus(WebdavStatus.SC_CREATED); // Copying source to destination. Hashtable<String, Integer> errorList = new Hashtable<String, Integer>(); boolean result; try { result = copyResource(errorList, path, destinationPath, req); } catch (RpcException e) { resp.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR); return false; } if (!result || !errorList.isEmpty()) { sendReport(req, resp, errorList); return false; } return true; } /** * Copy a collection. * * @param errorList Hashtable containing the list of errors which occurred * during the copy operation * @param source Path of the resource to be copied * @param theDest Destination path * @param req the HTTP request * @return boolean true if the copy is successful * @throws RpcException */ private boolean copyResource(Hashtable<String, Integer> errorList, String source, String theDest, HttpServletRequest req) throws RpcException { String dest = theDest; // Fix the destination path when copying collections. if (source.endsWith("/") && !dest.endsWith("/")) dest += "/"; if (logger.isDebugEnabled()) logger.debug("Copy: " + source + " To: " + dest); final User user = getUser(req); Object object = null; try { object = getService().getResourceAtPath(user.getId(), source, true); } catch (ObjectNotFoundException e) { } if (object instanceof Folder) { final Folder folderLocal = (Folder) object; try { final String des = dest; new TransactionHelper<Void>().tryExecute(new Callable<Void>() { @Override public Void call() throws Exception { getService().copyFolder(user.getId(), folderLocal.getId(), des); return null; } }); } catch (ObjectNotFoundException e) { errorList.put(dest, new Integer(WebdavStatus.SC_CONFLICT)); return false; } catch (DuplicateNameException e) { errorList.put(dest, new Integer(WebdavStatus.SC_CONFLICT)); return false; } catch (InsufficientPermissionsException e) { errorList.put(dest, new Integer(WebdavStatus.SC_FORBIDDEN)); return false; } catch (Exception e) { errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; } try { String newSource = source; if (!source.endsWith("/")) newSource += "/"; String newDest = dest; if (!dest.endsWith("/")) newDest += "/"; // Recursively copy the subfolders. Iterator iter = folderLocal.getSubfolders().iterator(); while (iter.hasNext()) { Folder subf = (Folder) iter.next(); String resourceName = subf.getName(); copyResource(errorList, newSource + resourceName, newDest + resourceName, req); } // Recursively copy the files. List<FileHeader> files; files = getService().getFiles(user.getId(), folderLocal.getId(), true); for (FileHeader file : files) { String resourceName = file.getName(); copyResource(errorList, newSource + resourceName, newDest + resourceName, req); } } catch (RpcException e) { errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; } catch (ObjectNotFoundException e) { errorList.put(source, new Integer(WebdavStatus.SC_NOT_FOUND)); return false; } catch (InsufficientPermissionsException e) { errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN)); return false; } } else if (object instanceof FileHeader) { final FileHeader fileLocal = (FileHeader) object; try { final String des = dest; new TransactionHelper<Void>().tryExecute(new Callable<Void>() { @Override public Void call() throws Exception { getService().copyFile(user.getId(), fileLocal.getId(), des); return null; } }); } catch (ObjectNotFoundException e) { errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; } catch (DuplicateNameException e) { errorList.put(source, new Integer(WebdavStatus.SC_CONFLICT)); return false; } catch (InsufficientPermissionsException e) { errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN)); return false; } catch (QuotaExceededException e) { errorList.put(source, new Integer(WebdavStatus.SC_FORBIDDEN)); return false; } catch (GSSIOException e) { errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; } catch (Exception e) { errorList.put(dest, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; } } else { errorList.put(source, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); return false; } return true; } /** * Delete a resource. * * @param req Servlet request * @param resp Servlet response * @return boolean true if the deletion is successful * @throws IOException */ private boolean deleteResource(HttpServletRequest req, HttpServletResponse resp) throws IOException { String path = getRelativePath(req); return deleteResource(path, req, resp, true); } /** * Delete a resource. * * @param path Path of the resource which is to be deleted * @param req Servlet request * @param resp Servlet response * @param setStatus Should the response status be set on successful * completion * @return boolean true if the deletion is successful * @throws IOException */ private boolean deleteResource(String path, HttpServletRequest req, HttpServletResponse resp, boolean setStatus) throws IOException { if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) { resp.sendError(WebdavStatus.SC_FORBIDDEN); return false; } String ifHeader = req.getHeader("If"); if (ifHeader == null) ifHeader = ""; String lockTokenHeader = req.getHeader("Lock-Token"); if (lockTokenHeader == null) lockTokenHeader = ""; if (isLocked(path, ifHeader + lockTokenHeader)) { resp.sendError(WebdavStatus.SC_LOCKED); return false; } final User user = getUser(req); boolean exists = true; Object object = null; try { object = getService().getResourceAtPath(user.getId(), path, true); } catch (ObjectNotFoundException e) { exists = false; } catch (RpcException e) { resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); return false; } if (!exists) { resp.sendError(WebdavStatus.SC_NOT_FOUND); return false; } Folder folderLocal = null; FileHeader fileLocal = null; if (object instanceof Folder) folderLocal = (Folder) object; else fileLocal = (FileHeader) object; if (fileLocal != null) try { final FileHeader f = fileLocal; new TransactionHelper<Void>().tryExecute(new Callable<Void>() { @Override public Void call() throws Exception { getService().deleteFile(user.getId(), f.getId()); return null; } }); } catch (InsufficientPermissionsException e) { resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); return false; } catch (ObjectNotFoundException e) { // Although we had already found the object, it was // probably deleted from another thread. resp.sendError(WebdavStatus.SC_NOT_FOUND); return false; } catch (RpcException e) { resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); return false; } catch (Exception e) { resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); return false; } else if (folderLocal != null) { Hashtable<String, Integer> errorList = new Hashtable<String, Integer>(); deleteCollection(req, folderLocal, path, errorList); try { final Folder f = folderLocal; new TransactionHelper<Void>().tryExecute(new Callable<Void>() { @Override public Void call() throws Exception { getService().deleteFolder(user.getId(), f.getId()); return null; } }); } catch (InsufficientPermissionsException e) { errorList.put(path, new Integer(WebdavStatus.SC_METHOD_NOT_ALLOWED)); } catch (ObjectNotFoundException e) { errorList.put(path, new Integer(WebdavStatus.SC_NOT_FOUND)); } catch (RpcException e) { errorList.put(path, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); } catch (Exception e) { errorList.put(path, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); } if (!errorList.isEmpty()) { sendReport(req, resp, errorList); return false; } } if (setStatus) resp.setStatus(WebdavStatus.SC_NO_CONTENT); return true; } /** * Deletes a collection. * * @param req the HTTP request * @param folder the folder whose contents will be deleted * @param path Path to the collection to be deleted * @param errorList Contains the list of the errors which occurred */ private void deleteCollection(HttpServletRequest req, Folder folder, String path, Hashtable<String, Integer> errorList) { if (logger.isDebugEnabled()) logger.debug("Delete:" + path); if (path.toUpperCase().startsWith("/WEB-INF") || path.toUpperCase().startsWith("/META-INF")) { errorList.put(path, new Integer(WebdavStatus.SC_FORBIDDEN)); return; } String ifHeader = req.getHeader("If"); if (ifHeader == null) ifHeader = ""; String lockTokenHeader = req.getHeader("Lock-Token"); if (lockTokenHeader == null) lockTokenHeader = ""; Iterator iter = folder.getSubfolders().iterator(); while (iter.hasNext()) { Folder subf = (Folder) iter.next(); String childName = path; if (!childName.equals("/")) childName += "/"; childName += subf.getName(); if (isLocked(childName, ifHeader + lockTokenHeader)) errorList.put(childName, new Integer(WebdavStatus.SC_LOCKED)); else try { final User user = getUser(req); Object object = getService().getResourceAtPath(user.getId(), childName, true); Folder childFolder = null; FileHeader childFile = null; if (object instanceof Folder) childFolder = (Folder) object; else childFile = (FileHeader) object; if (childFolder != null) { final Folder cf = childFolder; deleteCollection(req, childFolder, childName, errorList); new TransactionHelper<Void>().tryExecute(new Callable<Void>() { @Override public Void call() throws Exception { getService().deleteFolder(user.getId(), cf.getId()); return null; } }); } else if (childFile != null) { final FileHeader cf = childFile; new TransactionHelper<Void>().tryExecute(new Callable<Void>() { @Override public Void call() throws Exception { getService().deleteFile(user.getId(), cf.getId()); return null; } }); } } catch (ObjectNotFoundException e) { errorList.put(childName, new Integer(WebdavStatus.SC_NOT_FOUND)); } catch (InsufficientPermissionsException e) { errorList.put(childName, new Integer(WebdavStatus.SC_FORBIDDEN)); } catch (RpcException e) { errorList.put(childName, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); } catch (Exception e) { errorList.put(childName, new Integer(WebdavStatus.SC_INTERNAL_SERVER_ERROR)); } } } /** * Send a multistatus element containing a complete error report to the * client. * * @param req Servlet request * @param resp Servlet response * @param errorList List of error to be displayed * @throws IOException */ private void sendReport(HttpServletRequest req, HttpServletResponse resp, Hashtable errorList) throws IOException { resp.setStatus(WebdavStatus.SC_MULTI_STATUS); String absoluteUri = req.getRequestURI(); String relativePath = getRelativePath(req); XMLWriter generatedXML = new XMLWriter(); generatedXML.writeXMLHeader(); generatedXML.writeElement(null, "multistatus" + generateNamespaceDeclarations(), XMLWriter.OPENING); Enumeration pathList = errorList.keys(); while (pathList.hasMoreElements()) { String errorPath = (String) pathList.nextElement(); int errorCode = ((Integer) errorList.get(errorPath)).intValue(); generatedXML.writeElement(null, "response", XMLWriter.OPENING); generatedXML.writeElement(null, "href", XMLWriter.OPENING); String toAppend = errorPath.substring(relativePath.length()); if (!toAppend.startsWith("/")) toAppend = "/" + toAppend; generatedXML.writeText(absoluteUri + toAppend); generatedXML.writeElement(null, "href", XMLWriter.CLOSING); generatedXML.writeElement(null, "status", XMLWriter.OPENING); generatedXML.writeText("HTTP/1.1 " + errorCode + " " + WebdavStatus.getStatusText(errorCode)); generatedXML.writeElement(null, "status", XMLWriter.CLOSING); generatedXML.writeElement(null, "response", XMLWriter.CLOSING); } generatedXML.writeElement(null, "multistatus", XMLWriter.CLOSING); Writer writer = resp.getWriter(); writer.write(generatedXML.toString()); writer.close(); } // --------------------------------------------- WebdavResolver Inner Class /** * Work around for XML parsers that don't fully respect * {@link DocumentBuilderFactory#setExpandEntityReferences(boolean)}. * External references are filtered out for security reasons. See * CVE-2007-5461. */ private class WebdavResolver implements EntityResolver { /** * A private copy of the servlet context. */ private ServletContext context; /** * Construct the resolver by passing the servlet context. * * @param theContext the servlet context */ public WebdavResolver(ServletContext theContext) { context = theContext; } @Override public InputSource resolveEntity(String publicId, String systemId) { context.log("The request included a reference to an external entity with PublicID " + publicId + " and SystemID " + systemId + " which was ignored"); return new InputSource(new StringReader("Ignored external entity")); } } /** * Returns the user making the request. This is the user whose credentials * were supplied in the authorization header. * * @param req the HTTP request * @return the user making the request */ protected User getUser(HttpServletRequest req) { return (User) req.getAttribute(USER_ATTRIBUTE); } /** * Retrieves the user who owns the requested namespace, as specified in the * REST API. * * @param req the HTTP request * @return the owner of the namespace */ protected User getOwner(HttpServletRequest req) { return (User) req.getAttribute(OWNER_ATTRIBUTE); } /** * Check if the if-modified-since condition is satisfied. * * @param request The servlet request we are processing * @param response The servlet response we are creating * @param folder the folder object * @return boolean true if the resource meets the specified condition, and * false if the condition is not satisfied, in which case request * processing is stopped */ public boolean checkIfModifiedSince(HttpServletRequest request, HttpServletResponse response, Folder folder) { try { long headerValue = request.getDateHeader("If-Modified-Since"); long lastModified = folder.getAuditInfo().getModificationDate().getTime(); if (headerValue != -1) // If an If-None-Match header has been specified, if modified // since is ignored. if (request.getHeader("If-None-Match") == null && lastModified < headerValue + 1000) { // The entity has not been modified since the date // specified by the client. This is not an error case. response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return false; } } catch (IllegalArgumentException illegalArgument) { return true; } return true; } }