blob: 3a0f1eaf2f943645e48963226fa99f0c79aa75f2 [file] [log] [blame]
/*
* Copyright 1999-2004 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.components.source.impl;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;
import java.util.Enumeration;
import java.util.ArrayList;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.framework.logger.Logger;
import org.apache.cocoon.components.source.RestrictableSource;
import org.apache.cocoon.components.source.helpers.SourceCredential;
import org.apache.cocoon.components.source.helpers.SourcePermission;
import org.apache.cocoon.components.source.helpers.SourceProperty;
import org.apache.cocoon.components.source.InspectableSource;
import org.apache.cocoon.xml.XMLUtils;
import org.apache.commons.httpclient.HttpException;
import org.apache.excalibur.source.ModifiableTraversableSource;
import org.apache.excalibur.source.Source;
import org.apache.excalibur.source.SourceException;
import org.apache.excalibur.source.SourceValidity;
import org.apache.excalibur.source.impl.validity.TimeStampValidity;
import org.apache.util.HttpURL;
import org.apache.webdav.lib.WebdavResource;
import org.apache.webdav.lib.methods.DepthSupport;
import org.apache.webdav.lib.Property;
import org.apache.webdav.lib.PropertyName;
import org.apache.webdav.lib.ResponseEntity;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
/**
* A source implementation to get access to WebDAV repositories. Use it
* as webdav://[usr]:[password]@[host][:port]/path.
*
* @author <a href="mailto:g.casper@s-und-n.de">Guido Casper</a>
* @author <a href="mailto:gianugo@apache.org">Gianugo Rabellino</a>
* @author <a href="mailto:d.madama@pro-netics.com">Daniele Madama</a>
* @version $Id: WebDAVSource.java,v 1.20 2004/03/05 13:02:26 bdelacretaz Exp $
*/
public class WebDAVSource extends AbstractLogEnabled implements Source,
RestrictableSource, ModifiableTraversableSource, InspectableSource {
private static final String NAMESPACE = "http://apache.org/cocoon/webdav/1.0";
private static final String PREFIX = "webdav";
private static final String RESOURCE_NAME = "resource";
private static final String COLLECTION_NAME = "collection";
static {
WebdavResource.setGetUseDisk(false);
}
private String systemId;
private String location;
private String principal;
private String password;
private SourceValidity validity = null;
private long cachedLastModificationDate;
private SourceCredential sourcecredential = null;
private WebdavResource resource = null;
private String protocol;
private WebDAVSource(
String location,
String principal,
String password,
String protocol,
boolean createNew)
throws HttpException, IOException {
this.location = location;
this.principal = principal;
this.password = password;
this.protocol = protocol;
this.systemId = "http://" + location;
HttpURL httpURL = new HttpURL(this.systemId);
httpURL.setUserInfo(principal, password);
if (createNew) {
this.resource = new WebdavResource(httpURL,
WebdavResource.NOACTION,
DepthSupport.DEPTH_1);
}
else {
this.resource = new WebdavResource(httpURL);
}
}
/**
* Static factory method to obtain a Source.
*/
public static WebDAVSource newWebDAVSource(String location,
String principal,
String password,
String protocol,
Logger logger) throws SourceException {
// FIXME: wild hack needed for writing to a new resource.
// if a resource doesn't exist, an exception
// will be thrown, unless such resource isn't created with the
// WebdavResouce.NOACTION flag. However, such flag cannot be
// used by default, since it won't allow to discover the
// properties of an exixting resource. So either we do this
// hack here or we fill properties on the fly when requested.
// This "solution" is scary, but the SWCL is pretty dumb.
WebDAVSource source;
try {
source = new WebDAVSource(location, principal, password, protocol, false);
} catch (HttpException he) {
try {
source = new WebDAVSource(location, principal, password, protocol, true);
} catch (HttpException finalHe) {
final String message = "Error creating the source.";
throw new SourceException(message, finalHe);
} catch (IOException e) {
final String message = "Error creating the source.";
throw new SourceException(message, e);
}
} catch (IOException e) {
final String message = "Error creating the source.";
throw new SourceException(message, e);
}
source.enableLogging(logger);
return source;
}
/**
* Constructor used by the Traversable methods to build children.
*/
private WebDAVSource (WebdavResource source)
throws HttpException, IOException {
this.resource = source;
this.systemId = source.getHttpURL().getURI();
//fix trailing slash
if (this.resource.isCollection() && (this.systemId.endsWith("/") == false)) {
this.systemId = this.systemId+"/";
HttpURL httpURL = new HttpURL(this.systemId);
this.resource.setHttpURL(httpURL);
}
}
/**
* Get the scheme for this Source (webdav://).
*/
public String getScheme() {
return this.protocol;
}
/**
* Return an <code>InputStream</code> object to read from the source.
* This is the data at the point of invocation of this method,
* so if this is Modifiable, you might get different content
* from two different invocations.
*/
public InputStream getInputStream() throws IOException, SourceException {
try {
if (this.resource.isCollection()) {
WebdavResource[] resources =
this.resource.listWebdavResources();
return resourcesToXml(resources);
} else {
BufferedInputStream bi = null;
bi = new BufferedInputStream(this.resource.getMethodData());
if (!this.resource.exists()) {
throw new HttpException(this.systemId + " does not exist");
}
return bi;
}
} catch (HttpException he) {
throw new SourceException("Could not get WebDAV resource " + getSecureURI(), he);
} catch (Exception e) {
throw new SourceException("Could not get WebDAV resource" + getSecureURI(), e);
}
}
/**
* Return the unique identifer for this source
*/
public String getURI() {
// Change http: to webdav:
//return "webdav:" + this.systemId.substring(5);
//add Source credentials
if (principal != null)
return "webdav://" + this.principal + ":" + this.password + "@" + this.systemId.substring(7);
else
return "webdav://" + this.systemId.substring(7);
}
/**
* Return the URI securely, without username and password
*
*/
protected String getSecureURI() {
return "webdav://" + this.systemId.substring(7);
}
/**
* Get the Validity object. This can either wrap the last modification
* date or the expires information or...
* If it is currently not possible to calculate such an information
* <code>null</code> is returned.
*/
public SourceValidity getValidity() {
// TODO: Implementation taken from HttpClientSource, who took it from URLSource: time for a separate impl?
final long lm = getLastModified();
if ( lm > 0 )
{
if ( lm == cachedLastModificationDate )
{
return validity;
}
cachedLastModificationDate = lm;
validity = new TimeStampValidity( lm );
return validity;
}
return null;
}
/**
* Refresh the content of this object after the underlying data
* content has changed.
*/
public void refresh() {
try {
this.resource = new WebdavResource(this.systemId);
if (sourcecredential != null)
resource.setUserInfo(
sourcecredential.getPrincipal(),
sourcecredential.getPassword());
} catch (HttpException he) {
throw new IllegalStateException(he.getMessage());
} catch (IOException ioe) {
throw new IllegalStateException(ioe.getMessage());
}
this.validity = null;
}
/**
* The mime-type of the content described by this object.
* If the source is not able to determine the mime-type by itself
* this can be <code>null</code>.
*/
public String getMimeType() {
return this.resource.getGetContentType();
}
/**
* Return the content length of the content or -1 if the length is
* unknown
*/
public long getContentLength() {
if (this.resource.isCollection()) {
return -1;
}
return this.resource.getGetContentLength();
}
/**
* Get the last modification date.
* @return The last modification in milliseconds since January 1, 1970 GMT
* or 0 if it is unknown
*/
public long getLastModified() {
return this.resource.getGetLastModified();
}
/**
* Get the value of a parameter.
* Using this it is possible to get custom information provided by the
* source implementation, like an expires date, HTTP headers etc.
*/
public String getParameter(String name) {
return null;
}
/**
* Get the value of a parameter.
* Using this it is possible to get custom information provided by the
* source implementation, like an expires date, HTTP headers etc.
*/
public long getParameterAsLong(String name) {
return -1;
}
/**
* Get parameter names
* Using this it is possible to get custom information provided by the
* source implementation, like an expires date, HTTP headers etc.
*/
public Iterator getParameterNames() {
return null;
}
/**
* Does this source actually exist ?
*
* @return true if the resource exists.
*/
public boolean exists() {
return this.resource.getExistence();
}
/**
* Get an <code>InputStream</code> where raw bytes can be written to.
* The signification of these bytes is implementation-dependent and
* is not restricted to a serialized XML document.
*
* @return a stream to write to
*/
public OutputStream getOutputStream() throws IOException {
return new WebDAVSourceOutputStream(this.resource);
}
/**
* Can the data sent to an <code>OutputStream</code> returned by
* {@link #getOutputStream()} be cancelled ?
*
* @return true if the stream can be cancelled
*/
public boolean canCancel(OutputStream stream) {
return true;
}
/**
* Cancel the data sent to an <code>OutputStream</code> returned by
* {@link #getOutputStream()}.
* <p>
* After cancel, the stream should no more be used.
*/
public void cancel(OutputStream stream) throws SourceException {
// The content will only be send, if outputstream.close() executed.
}
/**
* Get the current credential for the source
*/
public SourceCredential getSourceCredential() throws SourceException {
return this.sourcecredential;
}
/**
* Set the credential for the source
*/
public void setSourceCredential(SourceCredential sourcecredential)
throws SourceException {
this.sourcecredential = sourcecredential;
if (sourcecredential == null) return;
try {
HttpURL httpURL = new HttpURL(this.systemId);
httpURL.setUserInfo(
sourcecredential.getPrincipal(),
sourcecredential.getPassword());
this.resource = new WebdavResource(httpURL);
} catch (HttpException he) {
throw new SourceException("Could not set credentials", he);
} catch (IOException ioe) {
throw new IllegalStateException(ioe.getMessage());
}
}
/**
* Set a permission to this source
*
* @param sourcepermission Permission, which should be set
*
* @throws SourceException If an exception occurs during this operation
*/
public void setSourcePermission(SourcePermission sourcepermission)
throws SourceException {
//FIXME
}
/**
* Returns a list of the existing permissions
*
* @return Array of SourcePermission
*/
public SourcePermission[] getSourcePermissions() throws SourceException {
//FIXME
return null;
}
public class WebDAVSourceOutputStream extends ByteArrayOutputStream {
private WebdavResource resource = null;
protected WebDAVSourceOutputStream(WebdavResource resource) {
this.resource = resource;
WebdavResource.setGetUseDisk(false);
}
public void close() throws IOException {
super.close();
try {
this.resource.putMethod(toByteArray());
} catch (HttpException he) {
final String message =
"Unable to close output stream. Server responded " +
he.getReasonCode() + " (" + he.getReason() + ") - "
+ he.getMessage();
getLogger().debug(message);
throw new IOException(he.getMessage());
}
}
}
/**
* Add a permission to this source
*
* @param sourcepermission Permission, which should be set
*
* @throws SourceException If an exception occurs during this operation
**/
public void addSourcePermission(SourcePermission sourcepermission)
throws SourceException {
// FIXME
}
/**
* Remove a permission from this source
*
* @param sourcepermission Permission, which should be removed
*
* @throws SourceException If an exception occurs during this operation
**/
public void removeSourcePermission(SourcePermission sourcepermission)
throws SourceException {
// FIXME
}
private InputStream resourcesToXml(WebdavResource[] resources)
throws Exception {
TransformerFactory tf = TransformerFactory.newInstance();
TransformerHandler th =
((SAXTransformerFactory) tf).newTransformerHandler();
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
StreamResult result = new StreamResult(bOut);
th.setResult(result);
th.startDocument();
th.startPrefixMapping(PREFIX, NAMESPACE);
th.startElement(NAMESPACE, COLLECTION_NAME,
PREFIX + ":" + COLLECTION_NAME, new AttributesImpl());
this.resourcesToSax(resources, th);
th.endElement(NAMESPACE, COLLECTION_NAME,
PREFIX + ":" + COLLECTION_NAME);
th.endPrefixMapping(PREFIX);
th.endDocument();
return new ByteArrayInputStream(bOut.toByteArray());
}
private void resourcesToSax(
WebdavResource[] resources,
ContentHandler handler)
throws SAXException {
for (int i = 0; i < resources.length; i++) {
if (getLogger().isDebugEnabled()) {
final String message =
"RESOURCE: " + resources[i].getDisplayName();
getLogger().debug(message);
}
if (resources[i].isCollection()) {
try {
WebdavResource[] childs =
resources[i].listWebdavResources();
AttributesImpl attrs = new AttributesImpl();
attrs.addAttribute(
NAMESPACE,
COLLECTION_NAME,
PREFIX + ":name",
"CDATA",
resources[i].getDisplayName());
handler.startElement(
NAMESPACE,
COLLECTION_NAME,
PREFIX + ":" + COLLECTION_NAME,
attrs);
this.resourcesToSax(childs, handler);
handler.endElement(
NAMESPACE,
COLLECTION_NAME,
PREFIX + ":" + COLLECTION_NAME);
} catch (HttpException e) {
if (getLogger().isDebugEnabled()) {
final String message =
"Unable to get WebDAV children. Server responded " +
e.getReasonCode() + " (" + e.getReason() + ") - "
+ e.getMessage();
getLogger().debug(message);
}
} catch (SAXException e) {
if (getLogger().isDebugEnabled()) {
final String message =
"Unable to get WebDAV children: "
+ e.getMessage();
getLogger().debug(message,e);
}
} catch (IOException e) {
if (getLogger().isDebugEnabled()) {
final String message =
"Unable to get WebDAV children: "
+ e.getMessage();
getLogger().debug(message,e);
}
} catch (Exception e) {
if (getLogger().isDebugEnabled()) {
final String message =
"Unable to get WebDAV children: "
+ e.getMessage();
getLogger().debug(message,e);
}
}
} else {
AttributesImpl attrs = new AttributesImpl();
attrs.addAttribute(
NAMESPACE,
"name",
PREFIX + ":name",
"CDATA",
resources[i].getDisplayName());
handler.startElement(
NAMESPACE,
RESOURCE_NAME,
PREFIX + ":" + RESOURCE_NAME,
attrs);
handler.endElement(
NAMESPACE,
RESOURCE_NAME,
PREFIX + ":" + RESOURCE_NAME);
}
}
}
/**
* Get a collection child.
*
* @see org.apache.excalibur.source.TraversableSource#getChild(java.lang.String)
*/
public Source getChild(String childName) throws SourceException {
String childLocation = this.location + "/" + childName;
WebDAVSource source = WebDAVSource.newWebDAVSource(
childLocation,
this.principal,
this.password,
this.protocol,
this.getLogger());
source.setSourceCredential(this.getSourceCredential());
return source;
}
/**
* Get the collection children.
*
* @see org.apache.excalibur.source.TraversableSource#getChildren()
*/
public Collection getChildren() throws SourceException {
ArrayList children = new ArrayList();
try {
WebdavResource[] resources = this.resource.listWebdavResources();
for (int i = 0; i < resources.length; i++) {
WebDAVSource src = new WebDAVSource(resources[i]);
children.add(src);
}
} catch (HttpException e) {
if (getLogger().isDebugEnabled()) {
final String message =
"Unable to get WebDAV children. Server responded " +
e.getReasonCode() + " (" + e.getReason() + ") - "
+ e.getMessage();
getLogger().debug(message);
}
throw new SourceException("Failed to get WebDAV collection children.", e);
} catch (IOException e) {
throw new SourceException("Failed to get WebDAV collection children.", e);
}
return children;
}
/**
* Get the name of this resource.
* @see org.apache.excalibur.source.TraversableSource#getName()
*/
public String getName() {
return this.resource.getDisplayName();
}
/**
* Get the parent.
* @see org.apache.excalibur.source.TraversableSource#getParent()
*/
public Source getParent() throws SourceException {
int last = this.location.lastIndexOf("/");
String myLocation = this.location.substring(0, last);
WebDAVSource wds = WebDAVSource.newWebDAVSource(
myLocation,
this.principal,
this.password,
this.protocol,
this.getLogger());
wds.setSourceCredential(this.getSourceCredential());
return wds;
}
/**
* Check if this source is a collection.
* @see org.apache.excalibur.source.TraversableSource#isCollection()
*/
public boolean isCollection() {
return this.resource.isCollection();
}
/**
* Delete this source (unimplemented).
* @see org.apache.excalibur.source.ModifiableSource#delete()
*/
public void delete() throws SourceException {
try {
this.resource.deleteMethod();
} catch (HttpException e) {
throw new SourceException("Unable to delete source: " + getSecureURI(), e);
} catch (IOException e) {
throw new SourceException("Unable to delete source: " + getSecureURI(), e);
}
}
/**
* Create the collection, if it doesn't exist.
* @see org.apache.excalibur.source.ModifiableTraversableSource#makeCollection()
*/
public void makeCollection() throws SourceException {
if (resource.exists()) return;
try {
resource.mkcolMethod();
} catch (HttpException e) {
throw new SourceException("Unable to create collection(s) " + getSecureURI(), e);
} catch (IOException e) {
throw new SourceException("Unable to create collection(s)" + getSecureURI(), e);
}
}
/**
* Returns a enumeration of the properties
*
* @return Enumeration of SourceProperty
*
* @throws SourceException If an exception occurs.
*/
public SourceProperty[] getSourceProperties() throws SourceException {
Vector sourceproperties = new Vector();
Enumeration props= null;
org.apache.webdav.lib.Property prop = null;
try {
Enumeration responses = this.resource.propfindMethod(0);
while (responses.hasMoreElements()) {
ResponseEntity response = (ResponseEntity)responses.nextElement();
props = response.getProperties();
while (props.hasMoreElements()) {
prop = (Property) props.nextElement();
SourceProperty srcProperty = new SourceProperty(prop.getElement());
sourceproperties.addElement(srcProperty);
}
}
} catch (Exception e) {
throw new SourceException("Error getting properties", e);
}
SourceProperty[] sourcepropertiesArray = new SourceProperty[sourceproperties.size()];
for (int i = 0; i<sourceproperties.size(); i++) {
sourcepropertiesArray[i] = (SourceProperty) sourceproperties.elementAt(i);
}
return sourcepropertiesArray;
}
/**
* Returns a property from a source.
*
* @param namespace Namespace of the property
* @param name Name of the property
*
* @return Property of the source.
*
* @throws SourceException If an exception occurs.
*/
public SourceProperty getSourceProperty (String namespace, String name)
throws SourceException {
Vector propNames = new Vector(1);
propNames.add(new PropertyName(namespace,name));
Enumeration props= null;
org.apache.webdav.lib.Property prop = null;
try {
Enumeration responses = this.resource.propfindMethod(0, propNames);
while (responses.hasMoreElements()) {
ResponseEntity response = (ResponseEntity)responses.nextElement();
props = response.getProperties();
if (props.hasMoreElements()) {
prop = (Property) props.nextElement();
return new SourceProperty(prop.getElement());
}
}
} catch (Exception e) {
throw new SourceException("Error getting property: "+name, e);
}
return null;
}
/**
* Remove a specified source property.
*
* @param namespace Namespace of the property.
* @param name Name of the property.
*
* @throws SourceException If an exception occurs.
*/
public void removeSourceProperty(String namespace, String name)
throws SourceException {
try {
this.resource.proppatchMethod(new PropertyName(namespace, name), "", false);
} catch (Exception e) {
throw new SourceException("Could not remove property ", e);
}
}
/**
* Sets a property for a source.
*
* @param sourceproperty Property of the source
*
* @throws SourceException If an exception occurs during this operation
*/
public void setSourceProperty(SourceProperty sourceproperty)
throws SourceException {
try {
Node node = null;
NodeList list = sourceproperty.getValue().getChildNodes();
for (int i=0; i<list.getLength(); i++) {
if ((list.item(i) instanceof Text && !"".equals(list.item(i).getNodeValue()))
|| list.item(i) instanceof Element) {
node = list.item(i);
break;
}
}
Properties format = new Properties();
format.put(OutputKeys.METHOD, "xml");
format.put(OutputKeys.OMIT_XML_DECLARATION, "yes");
String prop = XMLUtils.serializeNode(node, format);
this.resource.proppatchMethod(
new PropertyName(sourceproperty.getNamespace(),sourceproperty.getName()),
prop, true);
} catch(HttpException e) {
final String message =
"Unable to set property. Server responded " +
e.getReasonCode() + " (" + e.getReason() + ") - "
+ e.getMessage();
getLogger().debug(message);
throw new SourceException("Could not set property ", e);
} catch (Exception e) {
throw new SourceException("Could not set property ", e);
}
}
}