/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-06 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* $Id: WebDAVServlet.java 2782 2006-02-25 18:55:49Z dizzzz $
*/
package org.exist.atom.http;
import java.io.IOException;
import java.util.Date;
import java.util.Enumeration;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.log4j.Logger;
import org.exist.EXistException;
import org.exist.atom.Atom;
import org.exist.atom.modules.AtomProtocol;
import org.exist.atom.util.DOM;
import org.exist.atom.util.DOMDB;
import org.exist.atom.util.DateFormatter;
import org.exist.atom.util.NodeHandler;
import org.exist.collections.Collection;
import org.exist.collections.IndexInfo;
import org.exist.collections.triggers.TriggerException;
import org.exist.dom.DocumentImpl;
import org.exist.dom.ElementImpl;
import org.exist.http.webdav.WebDAV;
import org.exist.http.webdav.WebDAVMethod;
import org.exist.http.webdav.WebDAVMethodFactory;
import org.exist.http.webdav.methods.Copy;
import org.exist.http.webdav.methods.Delete;
import org.exist.http.webdav.methods.Mkcol;
import org.exist.http.webdav.methods.Move;
import org.exist.http.webdav.methods.Put;
import org.exist.security.PermissionDeniedException;
import org.exist.security.UUIDGenerator;
import org.exist.security.User;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.storage.lock.Lock;
import org.exist.storage.txn.TransactionException;
import org.exist.storage.txn.TransactionManager;
import org.exist.storage.txn.Txn;
import org.exist.util.LockException;
import org.exist.util.MimeTable;
import org.exist.util.MimeType;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.value.StringValue;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Provides a WebDAV interface that also maintains the atom feed if it exists
* in the directory.
*
* @author wolf
* @author Alex Milowski
*/
public class WebDAVServlet extends HttpServlet {
protected final static Logger LOG = Logger.getLogger(WebDAVServlet.class);
class FindEntryByResource implements NodeHandler {
String path;
Element matching;
FindEntryByResource(String path) {
this.path = path;
this.matching = null;
}
public void process(Node parent,Node child) {
Element entry = (Element)child;
NodeList nl = entry.getElementsByTagNameNS(Atom.NAMESPACE_STRING,"content");
if (nl.getLength()!=0) {
if (path.equals(((Element)nl.item(0)).getAttribute("src"))) {
matching = entry;
}
}
}
public Element getEntry() {
return matching;
}
}
class AtomWebDAVMethodFactory extends WebDAVMethodFactory {
public WebDAVMethod create(String method, BrokerPool pool) {
if (method.equals("PUT")) {
return new AtomPut(pool);
} else if (method.equals("DELETE")) {
return new AtomDelete(pool);
} else if (method.equals("MKCOL")) {
return new AtomMkcol(pool);
} else if (method.equals("MOVE")) {
return new AtomMove(pool);
} else if (method.equals("COPY")) {
return new AtomCopy(pool);
} else {
return super.create(method,pool);
}
}
}
class AtomPut extends Put {
AtomPut(BrokerPool pool) {
super(pool);
}
public void process(User user, HttpServletRequest request, HttpServletResponse response, XmldbURI path)
throws ServletException, IOException
{
XmldbURI filename = path.lastSegment();
XmldbURI collUri = path.removeLastSegment();
DBBroker broker = null;
Collection collection = null;
boolean updateToExisting = false;
try {
try {
broker = pool.get(user);
collection = broker.openCollection(collUri, Lock.READ_LOCK);
updateToExisting = collection.getDocument(broker,filename)!=null;
} catch (EXistException ex) {
throw new ServletException("Exception while getting a broker from the pool.",ex);
}
super.process(user,request,response,path);
if (updateToExisting) {
// We do nothing right now
LOG.debug("Update to existing resource, skipping feed update.");
return;
}
} finally {
if (collection!=null) {
collection.release(Lock.READ_LOCK);
}
}
TransactionManager transact = pool.getTransactionManager();
Txn transaction = transact.beginTransaction();
DocumentImpl feedDoc = null;
try {
LOG.debug("Atom PUT collUri='"+collUri+"'; path="+filename+"';" );
if (collection == null || collection.hasChildCollection(filename)) {
// We're already in an error state from the WebDAV action so just return
LOG.debug("No collection or subcollection already exists.");
transact.abort(transaction);
return;
}
MimeType mime;
String contentType = request.getContentType();
if (contentType == null) {
mime = MimeTable.getInstance().getContentTypeFor(filename);
if (mime != null) {
contentType = mime.getName();
}
} else {
int p = contentType.indexOf(';');
if (p > -1) {
contentType = StringValue.trimWhitespace(contentType.substring(0, p));
}
mime = MimeTable.getInstance().getContentType(contentType);
}
if (mime == null) {
mime = MimeType.BINARY_TYPE;
}
LOG.debug("Acquiring lock on feed document...");
feedDoc = collection.getDocument(broker,AtomProtocol.FEED_DOCUMENT_URI);
feedDoc.getUpdateLock().acquire(Lock.WRITE_LOCK);
String title = request.getHeader("Title");
if (title==null) {
title = filename.toString();
}
String created = DateFormatter.toXSDDateTime(new Date());
ElementImpl feedRoot = (ElementImpl)feedDoc.getDocumentElement();
DOMDB.replaceTextElement(transaction,feedRoot,Atom.NAMESPACE_STRING,"updated",created,true);
String id = "urn:uuid:"+UUIDGenerator.getUUID();
Element mediaEntry = AtomProtocol.generateMediaEntry(id,created,title,filename.toString(),mime.getName());
DOMDB.appendChild(transaction,feedRoot,mediaEntry);
broker.storeXMLResource(transaction, feedDoc);
transact.commit(transaction);
} catch (TransactionException ex) {
transact.abort(transaction);
throw new ServletException("Cannot commit transaction.",ex);
} catch (ParserConfigurationException ex) {
transact.abort(transaction);
throw new ServletException("DOM implementation is misconfigured.",ex);
} catch (LockException ex) {
transact.abort(transaction);
throw new ServletException("Cannot acquire write lock.",ex);
} finally {
if (feedDoc!=null) {
feedDoc.getUpdateLock().release(Lock.WRITE_LOCK);
}
if (collection!=null) {
collection.release(Lock.READ_LOCK);
}
pool.release(broker);
}
}
}
class AtomDelete extends Delete {
AtomDelete(BrokerPool pool) {
super(pool);
}
public void process(User user, HttpServletRequest request, HttpServletResponse response, XmldbURI path)
throws ServletException, IOException
{
super.process(user,request,response,path);
TransactionManager transact = pool.getTransactionManager();
Txn transaction = transact.beginTransaction();
DocumentImpl feedDoc = null;
DBBroker broker = null;
Collection collection = null;
try {
broker = pool.get(user);
XmldbURI filename = path.lastSegment();
XmldbURI collUri = path.removeLastSegment();
LOG.debug("Atom DELETE collUri='"+collUri+"'; path="+filename+"';" );
collection = broker.openCollection(collUri, Lock.READ_LOCK);
if (collection == null || collection.hasChildCollection(filename)) {
// We're already in an error state from the WebDAV action so just return
transact.abort(transaction);
return;
}
feedDoc = collection.getDocument(broker,AtomProtocol.FEED_DOCUMENT_URI);
feedDoc.getUpdateLock().acquire(Lock.WRITE_LOCK);
// Find the entry
FindEntryByResource finder = new FindEntryByResource(filename.toString());
DOM.findChildren(feedDoc.getDocumentElement(),Atom.NAMESPACE_STRING,"entry",finder);
Element entry = finder.getEntry();
if (entry!=null) {
// Remove the entry
ElementImpl feedRoot = (ElementImpl)feedDoc.getDocumentElement();
feedRoot.removeChild(transaction,entry);
// Update the feed time
String currentDateTime = DateFormatter.toXSDDateTime(new Date());
DOMDB.replaceTextElement(transaction,feedRoot,Atom.NAMESPACE_STRING,"updated",currentDateTime,true);
// Store the change on the feed
LOG.debug("Storing change...");
broker.storeXMLResource(transaction, feedDoc);
transact.commit(transaction);
} else {
// the entry is missing, so ignore
transact.abort(transaction);
}
} catch (TransactionException ex) {
transact.abort(transaction);
throw new ServletException("Cannot commit transaction.",ex);
} catch (EXistException ex) {
transact.abort(transaction);
throw new ServletException("Exception while getting a broker from the pool.",ex);
} catch (LockException ex) {
transact.abort(transaction);
throw new ServletException("Cannot acquire write lock.",ex);
} finally {
if (feedDoc!=null) {
feedDoc.getUpdateLock().release(Lock.WRITE_LOCK);
}
if (collection!=null) {
collection.release(Lock.READ_LOCK);
}
pool.release(broker);
}
}
}
class AtomMkcol extends Mkcol {
AtomMkcol(BrokerPool pool) {
super(pool);
}
public void process(User user, HttpServletRequest request, HttpServletResponse response, XmldbURI path)
throws ServletException, IOException
{
DBBroker broker = null;
Collection collection = null;
try {
try {
broker = pool.get(user);
collection = broker.openCollection(path, Lock.READ_LOCK);
if (collection != null) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
"collection " + request.getPathInfo() + " already exists");
return;
}
} finally {
if (collection != null)
collection.release(Lock.READ_LOCK);
}
} catch (EXistException ex) {
throw new ServletException("Exception while getting a broker from the pool.",ex);
}
super.process(user,request,response,path);
collection = broker.openCollection(path, Lock.READ_LOCK);
if (collection==null) {
pool.release(broker);
return;
}
DocumentImpl feedDoc = null;
TransactionManager transact = broker.getBrokerPool().getTransactionManager();
Txn transaction = transact.beginTransaction();
try {
String id = UUIDGenerator.getUUID();
String currentDateTime = DateFormatter.toXSDDateTime(new Date());
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
docFactory.setNamespaceAware(true);
Document doc = docFactory.newDocumentBuilder().getDOMImplementation().createDocument(Atom.NAMESPACE_STRING,"feed",null);
Element root = doc.getDocumentElement();
DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"id","urn:uuid:"+id,false);
DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"updated",currentDateTime,false);
DOM.replaceTextElement(root,Atom.NAMESPACE_STRING,"title",path.lastSegment().getCollectionPath(),false);
Element editLink = doc.createElementNS(Atom.NAMESPACE_STRING,"link");
editLink.setAttribute("rel","edit");
editLink.setAttribute("type",Atom.MIME_TYPE);
editLink.setAttribute("href","#");
root.appendChild(editLink);
IndexInfo info = collection.validateXMLResource(transaction,broker,AtomProtocol.FEED_DOCUMENT_URI,doc);
//TODO : we should probably unlock the collection here
collection.store(transaction,broker,info,doc,false);
transact.commit(transaction);
} catch (ParserConfigurationException ex) {
transact.abort(transaction);
throw new ServletException("SAX error: "+ex.getMessage(),ex);
} catch (TriggerException ex) {
transact.abort(transaction);
throw new ServletException("Trigger failed: "+ex.getMessage(),ex);
} catch (SAXException ex) {
transact.abort(transaction);
throw new ServletException("SAX error: "+ex.getMessage(),ex);
} catch (LockException ex) {
transact.abort(transaction);
throw new ServletException("Cannot acquire write lock.",ex);
} catch (PermissionDeniedException ex) {
transact.abort(transaction);
throw new ServletException("Permission denied.",ex);
} catch (EXistException ex) {
transact.abort(transaction);
throw new ServletException("Database exception",ex);
} finally {
collection.release(Lock.READ_LOCK);
pool.release(broker);
}
}
}
class AtomMove extends Move {
AtomMove(BrokerPool pool) {
super(pool);
}
}
class AtomCopy extends Copy {
AtomCopy(BrokerPool pool) {
super(pool);
}
}
private WebDAV webdav;
/** id of the database registred against the BrokerPool */
protected String databaseid = BrokerPool.DEFAULT_INSTANCE_NAME;
/* (non-Javadoc)
* @see javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
*/
public void init(ServletConfig config) throws ServletException {
super.init(config);
// <frederic.glorieux@ajlsm.com> to allow multi-instance webdav server,
// use a databaseid everywhere
String id = config.getInitParameter("database-id");
if (id != null && !"".equals(id)) this.databaseid=id;
int authMethod = WebDAV.DIGEST_AUTH;
String param = config.getInitParameter("authentication");
if(param != null && "basic".equalsIgnoreCase(param))
authMethod = WebDAV.BASIC_AUTH;
webdav = new WebDAV(authMethod, this.databaseid,new AtomWebDAVMethodFactory());
}
/* (non-Javadoc)
* @see javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
dumpHeaders(request);
webdav.process(request, response);
}
private void dumpHeaders(HttpServletRequest request) {
System.out.println("-------------------------------------------------------");
System.out.println(request.getMethod()+" "+request.getPathInfo());
for(Enumeration e = request.getHeaderNames(); e.hasMoreElements(); ) {
String header = (String)e.nextElement();
System.out.println(header + " = " + request.getHeader(header));
}
}
}