/******************************************************************************* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.wink.webdav.server; import java.io.IOException; import java.io.StringReader; import java.net.URI; import java.util.List; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import org.apache.wink.common.http.HttpStatus; import org.apache.wink.common.internal.i18n.Messages; import org.apache.wink.common.model.synd.SyndBase; import org.apache.wink.common.model.synd.SyndEntry; import org.apache.wink.common.model.synd.SyndFeed; import org.apache.wink.common.model.synd.SyndLink; import org.apache.wink.webdav.model.Allprop; import org.apache.wink.webdav.model.Collection; import org.apache.wink.webdav.model.Creationdate; import org.apache.wink.webdav.model.Displayname; import org.apache.wink.webdav.model.Error; import org.apache.wink.webdav.model.Getcontentlanguage; import org.apache.wink.webdav.model.Getcontentlength; import org.apache.wink.webdav.model.Getcontenttype; import org.apache.wink.webdav.model.Getetag; import org.apache.wink.webdav.model.Getlastmodified; import org.apache.wink.webdav.model.Lockdiscovery; import org.apache.wink.webdav.model.Multistatus; import org.apache.wink.webdav.model.Prop; import org.apache.wink.webdav.model.Propfind; import org.apache.wink.webdav.model.Propstat; import org.apache.wink.webdav.model.Resourcetype; import org.apache.wink.webdav.model.Supportedlock; import org.apache.wink.webdav.model.WebDAVModelHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Element; import org.w3c.dom.NodeList; public class WebDAVResponseBuilder { private static final Logger logger = LoggerFactory.getLogger(WebDAVResponseBuilder.class); private UriInfo uriInfo; private WebDAVResponseBuilder(UriInfo uriInfo) { this.uriInfo = uriInfo; } public static WebDAVResponseBuilder create(UriInfo uriInfo) { return new WebDAVResponseBuilder(uriInfo); } /** * Process the PROPFIND request for a given entry and create a response * using the default implementation of {@link PropertyHandler} * * @param entry the entry containing the data to use to create the propfind * response * @param propfindXml the propfind xml request to create the response for * @return a response to the profind request * @throws IOException */ public Response propfind(SyndEntry entry, String propfindXml) throws IOException { return propfind(entry, propfindXml, new PropertyHandler()); } /** * Process the PROPFIND request for given document resource and create a * response. * * @param entry the entry containing the data to use to create the propfind * response * @param propfindXml the propfind xml request to create the response for * @param handler a {@link PropertyHandler} that will be used to retrieve * the values of properties for the response * @return a response to the profind request * @throws IOException */ public Response propfind(SyndEntry entry, String propfindXml, PropertyHandler handler) throws IOException { // parse the request (no content means 'all properties') Propfind propfind = null; if (propfindXml == null || propfindXml.length() == 0) { propfind = new Propfind(); propfind.setAllprop(new Allprop()); } else { propfind = Propfind.unmarshal(new StringReader(propfindXml)); } // make the response Multistatus multistatus = new Multistatus(); // fill the multistatus object with the response addResponseToMultistatus(multistatus, propfind, entry, handler); // HTTP response Response httpResponse = Response.status(HttpStatus.MULTI_STATUS.getCode()).entity(multistatus).build(); return httpResponse; } /** * Process the PROPFIND request for the given feed and create a response * using the default implementation of {@link CollectionPropertyHandler} * * @param feed the feed containing the data to use to create the propfind * response * @param propfindXml the propfind xml request to create the response for * @param depthStr the value of the Depth header * @return a response to the profind request * @throws IOException */ public Response propfind(SyndFeed feed, String propfindXml, String depthStr) throws IOException { return propfind(feed, propfindXml, depthStr, new CollectionPropertyHandler()); } /** * Process the PROPFIND request for the given collection resource and create * a response. * * @param feed the feed containing the data to use to create the propfind * response * @param propfindXml the propfind xml request to create the response for * @param depthStr the value of the Depth header * @param provider a CollectionPropertyProvider that will be used to * retrieve the values of properties for the response * @return a response to the profind request * @throws IOException */ public Response propfind(SyndFeed feed, String propfindXml, String depthStr, CollectionPropertyHandler provider) throws IOException { // parse the request (no content means 'all properties') Propfind propfind = null; if (propfindXml == null || propfindXml.length() == 0) { propfind = new Propfind(); propfind.setAllprop(new Allprop()); } else { propfind = Propfind.unmarshal(new StringReader(propfindXml)); } // make the response Multistatus multistatus = new Multistatus(); // root collection addResponseToMultistatus(multistatus, propfind, feed, provider); // sub-collections and entries // get Depth header int depth = 1; // the default depth should be infinity but we support // only 0 or 1 // String strDepth = requestHeaders.getFirst(WebDAVHeaders.DEPTH); if (depthStr != null) { depth = Integer.parseInt(depthStr); } // limit depth to 0 or 1 - robust behaviour (do not report an error) if (depth < 0) { depth = 0; } else if (depth > 1) { depth = 1; } if (depth > 0) { // depth == 1 // entries for (SyndEntry entry : feed.getEntries()) { entry.setBase(feed.getBase()); // use the feed URI base if (provider.isSubCollection(entry)) { // sub-collection SyndFeed subCollection = provider.getSubCollection(entry); if (subCollection != null) { addResponseToMultistatus(multistatus, propfind, subCollection, provider); } } else { // entry addResponseToMultistatus(multistatus, propfind, entry, provider .getEntryPropertyHandler()); } } // sub-collections List<SyndFeed> subCollections = provider.getSubCollections(this, feed); if (subCollections != null) { for (SyndFeed subCollection : subCollections) { addResponseToMultistatus(multistatus, propfind, subCollection, provider); } } } // HTTP response Response httpResponse = Response.status(HttpStatus.MULTI_STATUS.getCode()).entity(multistatus).build(); return httpResponse; } /** * Adds a WebDAV response element to the given multistatus element. * * @param multistatus the multistatus response * @param propfind the propfind request * @param synd either feed or entry * @param handler the property handler to use to set property values */ private void addResponseToMultistatus(Multistatus multistatus, Propfind propfind, SyndBase synd, PropertyHandler handler) { // create response and add it to the multistatus object org.apache.wink.webdav.model.Response response = new org.apache.wink.webdav.model.Response(); response.getHref().add(getResourceLink(synd)); multistatus.getResponse().add(response); // the request is for all property names if (propfind.getPropname() != null) { Propstat propstat = response.getOrCreatePropstat(Response.Status.OK.getStatusCode(), null, null); Prop prop = propstat.getProp(); // call the abstract method to allow the handler to fill in the // property names handler.setAllPropertyNames(this, prop, synd); // ensure that all property values are empty ensurePropertiesAreEmpty(prop); } else { Prop prop = null; // the request is for all properties if (propfind.getAllprop() != null) { prop = new Prop(); handler.setAllPropertyNames(this, prop, synd); } else { // the request is for specific properties prop = propfind.getProp(); } setPropertyValues(response, prop, synd, handler); } } /** * Set property values in the provided Response instance. The properties to * set are indicated in the provided Prop object. * * @param prop the Prop instance to fill with values */ private void setPropertyValues(org.apache.wink.webdav.model.Response response, Prop sourceProp, SyndBase synd, PropertyHandler provider) { if (sourceProp == null || response == null) { return; } if (sourceProp.getCreationdate() != null) { provider.setPropertyValue(this, response, new Creationdate(), synd); } if (sourceProp.getDisplayname() != null) { provider.setPropertyValue(this, response, new Displayname(), synd); } if (sourceProp.getGetcontentlanguage() != null) { provider.setPropertyValue(this, response, new Getcontentlanguage(), synd); } if (sourceProp.getGetcontentlength() != null) { provider.setPropertyValue(this, response, new Getcontentlength(), synd); } if (sourceProp.getGetcontenttype() != null) { provider.setPropertyValue(this, response, new Getcontenttype(), synd); } if (sourceProp.getGetetag() != null) { provider.setPropertyValue(this, response, new Getetag(), synd); } if (sourceProp.getGetlastmodified() != null) { provider.setPropertyValue(this, response, new Getlastmodified(), synd); } if (sourceProp.getLockdiscovery() != null) { provider.setPropertyValue(this, response, new Lockdiscovery(), synd); } if (sourceProp.getResourcetype() != null) { provider.setPropertyValue(this, response, new Resourcetype(), synd); } if (sourceProp.getSupportedlock() != null) { provider.setPropertyValue(this, response, new Supportedlock(), synd); } for (Element element : sourceProp.getAny()) { Element newElement = WebDAVModelHelper.createElement(element.getNamespaceURI(), element.getLocalName()); provider.setPropertyValue(this, response, newElement, synd); } } private void ensurePropertiesAreEmpty(Prop prop) { if (prop == null) { return; } if (prop.getCreationdate() != null) { prop.getCreationdate().setValue((String)null); } if (prop.getDisplayname() != null) { prop.getDisplayname().setValue(null); } if (prop.getGetcontentlanguage() != null) { prop.getGetcontentlanguage().setValue(null); } if (prop.getGetcontentlength() != null) { prop.getGetcontentlength().setValue(null); } if (prop.getGetcontenttype() != null) { prop.getGetcontenttype().setValue(null); } if (prop.getGetetag() != null) { prop.getGetetag().setValue(null); } if (prop.getGetlastmodified() != null) { prop.getGetlastmodified().setValue((String)null); } if (prop.getLockdiscovery() != null) { prop.getLockdiscovery().getActivelock().clear(); } if (prop.getResourcetype() != null) { prop.getResourcetype().setCollection(null); prop.getResourcetype().getAny().clear(); } if (prop.getSupportedlock() != null) { prop.getSupportedlock().getLockentry().clear(); } for (Element element : prop.getAny()) { // remove all child nodes (including text) NodeList nodes = element.getChildNodes(); if (nodes != null && nodes.getLength() > 0) { for (int i = 0; i < nodes.getLength(); ++i) { element.removeChild(nodes.item(0)); } } } } /** * Get the normalized URL of the resource by first trying the 'edit' link * and then the 'self' link. * * @param synd the synd to extract the link from * @return the URL of the resource * @throws WebApplicationException if neither the 'edit' nor the 'self' * links exist. */ private String getResourceLink(SyndBase synd) { // try 'edit' link SyndLink link = synd.getLink("edit"); //$NON-NLS-1$ if (link == null) { // try 'self' link link = synd.getLink("self"); //$NON-NLS-1$ } if (link == null) { // no link in the resource if (logger.isErrorEnabled()) { logger.error(Messages.getMessage("webDAVNoEditOrSelfLink", synd.getId())); //$NON-NLS-1$ } throw new WebApplicationException(); } URI uri = URI.create(link.getHref()).normalize(); if (!uri.isAbsolute()) { // add base URI for relative links URI base = uriInfo.getAbsolutePath(); if (synd.getBase() != null) { base = URI.create(synd.getBase()); } return base.resolve(uri).getRawPath(); // keep the path escaped } else { return uri.getRawPath(); // keep the path escaped } } /** * Used during the creation of a WebDAV multistatus response to get the * properties and their values. Applications may override any of the methods * as required. */ public static class PropertyHandler { /** * Set the Prop instance with empty (no values) properties. This method * is invoked to create a response for a propfind request containing a * propname element * * @param builder the current WebDAVResponseBuilder which can be used to * obtain context information * @param synd instance of the synd to set the property names for * @param prop the Prop instance to fill */ public void setAllPropertyNames(WebDAVResponseBuilder builder, Prop prop, SyndBase synd) { prop.setDisplayname(new Displayname()); prop.setGetlastmodified(new Getlastmodified()); prop.setResourcetype(new Resourcetype()); // also returns the creation date by default for an entry if (synd instanceof SyndEntry) { prop.setCreationdate(new Creationdate()); } } /** * Set the value of a provided property, and set the property on the * response object with the correct status. * <p> * Applications should override this method with their own * implementation in order to set the values of proprietary properties * and to extend the default behavior. * <p> * see {@link Response#setProperty(Object, int, String, Error)} for a * detailed description of the possible property values. * <p> * Examples: * <p> * if the property is <code>DAV:getcontentlanguage</code> then the type * of the property is an instance of Getcontentlanguage, and setting * it's value is performed as follows: * * <pre> * if (property instanceof Getcontentlanguage) { * ((Getcontentlanguage)property).setValue("en-us"); * response.setOk(property); * } * </pre> * * if the property is <code>K:myprop</code> then the type of the * property is an instance of org.w3c.dom.Element, and setting it's * value is performed as follows: * * <pre> * if (property instanceof Element) { * ((Element)property).setTextContent("My Property Value!"); * response.setOk(property); * } * </pre> * * If the requested property does not have a value, the application * should call the {@link Response#setPropertyNotFound(Object)} method * and pass it the property object, like so: * * <pre> * response.setNotFound(property); * </pre> * * @param builder the current WebDAVResponseBuilder which can be used to * obtain context information * @param response the {@link org.apache.wink.webdav.model.Response} * instance that is to receive the property and its value * @param property the property object. see * {@link org.apache.wink.webdav.model.Response#setProperty(Object, int, String, Error)} * for a detailed description of the possible property values * @param synd an instance of synd (either feed or entry) */ public void setPropertyValue(WebDAVResponseBuilder builder, org.apache.wink.webdav.model.Response response, Object property, SyndBase synd) { if (property instanceof Displayname) { if (synd.getTitle().getValue() != null) { ((Displayname)property).setValue(synd.getTitle().getValue()); response.setPropertyOk(property); return; } } else if (property instanceof Getlastmodified) { if (synd.getUpdated() != null) { ((Getlastmodified)property).setValue(synd.getUpdated()); response.setPropertyOk(property); return; } } if (synd instanceof SyndEntry) { SyndEntry entry = (SyndEntry)synd; if (property instanceof Creationdate) { if (entry.getPublished() != null) { ((Creationdate)property).setValue(entry.getPublished()); response.setPropertyOk(property); return; } } else if (property instanceof Resourcetype) { response.setPropertyOk(property); return; } } if (synd instanceof SyndFeed) { if (property instanceof Resourcetype) { ((Resourcetype)property).setCollection(new Collection()); response.setPropertyOk(property); return; } } response.setPropertyNotFound(property); } } /** * Extends the {@link PropertyHandler} to provide additional data for the * creation of multistatus responses for collections (feeds). */ public static class CollectionPropertyHandler extends PropertyHandler { private PropertyHandler entryPropertyHandler; /** * Constructs a CollectionPropertyProvider that is also the property * handler for the entries */ public CollectionPropertyHandler() { setEntryPropertyHandler(this); } /** * Constructor accepting another {@link PropertyHandler} to provide * properties for the entries in the collection. * * @param entryPropertyHandler */ public CollectionPropertyHandler(PropertyHandler entryPropertyHandler) { setEntryPropertyHandler(entryPropertyHandler); } /** * Gets the list of sub-collection feeds for a given feed. The default * implementation returns <code>null</code> * * @param builder the current WebDAVResponseBuilder which can be used to * obtain context information * @param feed the feed to obtain the sub collections from * @return the sub-collections */ public List<SyndFeed> getSubCollections(WebDAVResponseBuilder builder, SyndFeed feed) { return null; } /** * Specifies if an entry actually represents a feed. This method is * called only when building the response for an entry that is part of a * feed. The default implementation returns <code>false</code> * * @param entry the entry * @return <code>true</code> if the entry is a sub-collection, otherwise * <code>false</code> */ public boolean isSubCollection(SyndEntry entry) { return false; } /** * Get the feed that this entry represents. This method is called if the * {@link #isEntrySubCollection(DocumentResource)} method returns * <code>true</code> for the given entry. The default implementation * returns <code>null</code> * * @param entry the entry that represents a feed * @return an instance of a SyndFeed */ public SyndFeed getSubCollection(SyndEntry entry) { return null; } /** * Get the PropertyHandler that is used to set properties for the * entries in the collection * * @return the PropertyHandler used to set properties for the entries in * the collection */ public final PropertyHandler getEntryPropertyHandler() { return entryPropertyHandler; } /** * Set the PropertyHandler that is used to set properties for the * entries in the collection * * @param entryPropertyHandler the entry PropertyHandler */ public final void setEntryPropertyHandler(PropertyHandler entryPropertyHandler) { this.entryPropertyHandler = entryPropertyHandler; } } }