001/* 002 * (C) Copyright 2006-2009 Nuxeo SAS (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Nuxeo - initial API and implementation 016 * 017 * $Id$ 018 */ 019 020package org.nuxeo.ecm.webdav.resource; 021 022import static javax.ws.rs.core.Response.Status.OK; 023import static javax.ws.rs.core.Response.Status.FORBIDDEN; 024 025import java.io.UnsupportedEncodingException; 026import java.util.Calendar; 027import java.util.Date; 028import java.util.HashSet; 029import java.util.Set; 030 031import javax.servlet.http.HttpServletRequest; 032import javax.ws.rs.DELETE; 033import javax.ws.rs.HEAD; 034import javax.ws.rs.HeaderParam; 035import javax.ws.rs.core.Context; 036import javax.ws.rs.core.Response; 037import javax.ws.rs.core.UriInfo; 038 039import net.java.dev.webdav.jaxrs.methods.COPY; 040import net.java.dev.webdav.jaxrs.methods.LOCK; 041import net.java.dev.webdav.jaxrs.methods.MKCOL; 042import net.java.dev.webdav.jaxrs.methods.MOVE; 043import net.java.dev.webdav.jaxrs.methods.PROPPATCH; 044import net.java.dev.webdav.jaxrs.methods.UNLOCK; 045import net.java.dev.webdav.jaxrs.xml.elements.ActiveLock; 046import net.java.dev.webdav.jaxrs.xml.elements.Depth; 047import net.java.dev.webdav.jaxrs.xml.elements.HRef; 048import net.java.dev.webdav.jaxrs.xml.elements.LockRoot; 049import net.java.dev.webdav.jaxrs.xml.elements.LockScope; 050import net.java.dev.webdav.jaxrs.xml.elements.LockToken; 051import net.java.dev.webdav.jaxrs.xml.elements.LockType; 052import net.java.dev.webdav.jaxrs.xml.elements.MultiStatus; 053import net.java.dev.webdav.jaxrs.xml.elements.Owner; 054import net.java.dev.webdav.jaxrs.xml.elements.Prop; 055import net.java.dev.webdav.jaxrs.xml.elements.PropStat; 056import net.java.dev.webdav.jaxrs.xml.elements.Status; 057import net.java.dev.webdav.jaxrs.xml.elements.TimeOut; 058import net.java.dev.webdav.jaxrs.xml.properties.LockDiscovery; 059 060import org.apache.commons.httpclient.URIException; 061import org.apache.commons.httpclient.util.URIUtil; 062import org.apache.commons.lang.StringUtils; 063import org.apache.commons.logging.Log; 064import org.apache.commons.logging.LogFactory; 065import org.nuxeo.common.utils.Path; 066import org.nuxeo.ecm.core.api.Blob; 067import org.nuxeo.ecm.core.api.DocumentModel; 068import org.nuxeo.ecm.core.api.DocumentSecurityException; 069import org.nuxeo.ecm.core.api.NuxeoException; 070import org.nuxeo.ecm.core.api.PathRef; 071import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 072import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; 073import org.nuxeo.ecm.webdav.backend.Backend; 074import org.nuxeo.ecm.webdav.backend.BackendHelper; 075import org.nuxeo.ecm.webdav.jaxrs.Win32CreationTime; 076import org.nuxeo.ecm.webdav.jaxrs.Win32FileAttributes; 077import org.nuxeo.ecm.webdav.jaxrs.Win32LastAccessTime; 078import org.nuxeo.ecm.webdav.jaxrs.Win32LastModifiedTime; 079 080/** 081 * An existing resource corresponds to an existing object (folder or file) in the repository. 082 */ 083public class ExistingResource extends AbstractResource { 084 085 public static final String READONLY_TOKEN = "readonly"; 086 087 private static final Log log = LogFactory.getLog(ExistingResource.class); 088 089 protected DocumentModel doc; 090 091 protected Backend backend; 092 093 protected ExistingResource(String path, DocumentModel doc, HttpServletRequest request, Backend backend) { 094 super(path, request); 095 this.doc = doc; 096 this.backend = backend; 097 } 098 099 @DELETE 100 public Response delete() { 101 if (backend.isLocked(doc.getRef()) && !backend.canUnlock(doc.getRef())) { 102 return Response.status(423).build(); 103 } 104 105 try { 106 backend.removeItem(doc.getRef()); 107 backend.saveChanges(); 108 return Response.status(204).build(); 109 } catch (DocumentSecurityException e) { 110 log.error("Can't remove item: " + doc.getPathAsString() + e.getMessage()); 111 log.debug(e); 112 return Response.status(FORBIDDEN).build(); 113 } 114 } 115 116 @COPY 117 public Response copy(@HeaderParam("Destination") String dest, @HeaderParam("Overwrite") String overwrite) { 118 return copyOrMove("COPY", dest, overwrite); 119 } 120 121 @MOVE 122 public Response move(@HeaderParam("Destination") String dest, @HeaderParam("Overwrite") String overwrite) { 123 if (backend.isLocked(doc.getRef()) && !backend.canUnlock(doc.getRef())) { 124 return Response.status(423).build(); 125 } 126 127 return copyOrMove("MOVE", dest, overwrite); 128 } 129 130 private static String encode(byte[] bytes, String encoding) { 131 try { 132 return new String(bytes, encoding); 133 } catch (UnsupportedEncodingException e) { 134 throw new NuxeoException("Unsupported encoding " + encoding); 135 } 136 } 137 138 private Response copyOrMove(String method, @HeaderParam("Destination") String destination, 139 @HeaderParam("Overwrite") String overwrite) { 140 141 if (backend.isLocked(doc.getRef()) && !backend.canUnlock(doc.getRef())) { 142 return Response.status(423).build(); 143 } 144 145 destination = encode(destination.getBytes(), "ISO-8859-1"); 146 try { 147 destination = URIUtil.decode(destination); 148 } catch (URIException e) { 149 throw new NuxeoException(e); 150 } 151 152 Backend root = BackendHelper.getBackend("/", request); 153 Set<String> names = new HashSet<String>(root.getVirtualFolderNames()); 154 Path destinationPath = new Path(destination); 155 String[] segments = destinationPath.segments(); 156 int removeSegments = 0; 157 for (String segment : segments) { 158 if (names.contains(segment)) { 159 break; 160 } else { 161 removeSegments++; 162 } 163 } 164 destinationPath = destinationPath.removeFirstSegments(removeSegments); 165 166 String destPath = destinationPath.toString(); 167 String davDestPath = destPath; 168 Backend destinationBackend = BackendHelper.getBackend(davDestPath, request); 169 destPath = destinationBackend.parseLocation(destPath).toString(); 170 log.debug("to " + davDestPath); 171 172 // Remove dest if it exists and the Overwrite header is set to "T". 173 int status = 201; 174 if (destinationBackend.exists(davDestPath)) { 175 if ("F".equals(overwrite)) { 176 return Response.status(412).build(); 177 } 178 destinationBackend.removeItem(davDestPath); 179 status = 204; 180 } 181 182 // Check if parent exists 183 String destParentPath = getParentPath(destPath); 184 PathRef destParentRef = new PathRef(destParentPath); 185 if (!destinationBackend.exists(getParentPath(davDestPath))) { 186 return Response.status(409).build(); 187 } 188 189 if ("COPY".equals(method)) { 190 DocumentModel destDoc = backend.copyItem(doc, destParentRef); 191 backend.renameItem(destDoc, getNameFromPath(destPath)); 192 } else if ("MOVE".equals(method)) { 193 if (backend.isRename(doc.getPathAsString(), destPath)) { 194 backend.renameItem(doc, getNameFromPath(destPath)); 195 } else { 196 backend.moveItem(doc, destParentRef); 197 } 198 } 199 backend.saveChanges(); 200 return Response.status(status).build(); 201 } 202 203 // Properties 204 205 @PROPPATCH 206 public Response proppatch(@Context UriInfo uriInfo) { 207 if (backend.isLocked(doc.getRef()) && !backend.canUnlock(doc.getRef())) { 208 return Response.status(423).build(); 209 } 210 211 /* 212 * JAXBContext jc = Util.getJaxbContext(); Unmarshaller u = jc.createUnmarshaller(); PropertyUpdate 213 * propertyUpdate; try { propertyUpdate = (PropertyUpdate) u.unmarshal(request.getInputStream()); } catch 214 * (JAXBException e) { return Response.status(400).build(); } 215 */ 216 // Util.printAsXml(propertyUpdate); 217 /* 218 * List<RemoveOrSet> list = propertyUpdate.list(); final List<PropStat> propStats = new ArrayList<PropStat>(); 219 * for (RemoveOrSet set : list) { Prop prop = set.getProp(); List<Object> properties = prop.getProperties(); for 220 * (Object property : properties) { PropStat propStat = new PropStat(new Prop(property), new Status(OK)); 221 * propStats.add(propStat); } } 222 */ 223 224 // @TODO: patch properties if need. 225 // Fake proppatch response 226 @SuppressWarnings("deprecation") 227 final net.java.dev.webdav.jaxrs.xml.elements.Response response = new net.java.dev.webdav.jaxrs.xml.elements.Response( 228 new HRef(uriInfo.getRequestUri()), null, null, null, new PropStat(new Prop(new Win32CreationTime()), 229 new Status(OK)), new PropStat(new Prop(new Win32FileAttributes()), new Status(OK)), 230 new PropStat(new Prop(new Win32LastAccessTime()), new Status(OK)), new PropStat(new Prop( 231 new Win32LastModifiedTime()), new Status(OK))); 232 233 return Response.status(207).entity(new MultiStatus(response)).build(); 234 } 235 236 /** 237 * We can't MKCOL over an existing resource. 238 */ 239 @MKCOL 240 public Response mkcol() { 241 return Response.status(405).build(); 242 } 243 244 @HEAD 245 public Response head() { 246 return Response.status(200).build(); 247 } 248 249 @LOCK 250 public Response lock(@Context UriInfo uriInfo) { 251 String token = null; 252 Prop prop = null; 253 if (backend.isLocked(doc.getRef())) { 254 if (!backend.canUnlock(doc.getRef())) { 255 return Response.status(423).build(); 256 } else { 257 token = backend.getCheckoutUser(doc.getRef()); 258 prop = new Prop(getLockDiscovery(doc, uriInfo)); 259 return Response.ok().entity(prop).header("Lock-Token", "urn:uuid:" + token).build(); 260 } 261 } 262 263 token = backend.lock(doc.getRef()); 264 if (READONLY_TOKEN.equals(token)) { 265 return Response.status(423).build(); 266 } else if (StringUtils.isEmpty(token)) { 267 return Response.status(400).build(); 268 } 269 270 prop = new Prop(getLockDiscovery(doc, uriInfo)); 271 272 backend.saveChanges(); 273 return Response.ok().entity(prop).header("Lock-Token", "urn:uuid:" + token).build(); 274 } 275 276 @UNLOCK 277 public Response unlock() { 278 if (backend.isLocked(doc.getRef())) { 279 if (!backend.canUnlock(doc.getRef())) { 280 return Response.status(423).build(); 281 } else { 282 backend.unlock(doc.getRef()); 283 backend.saveChanges(); 284 return Response.status(204).build(); 285 } 286 } else { 287 // TODO: return an error 288 return Response.status(204).build(); 289 } 290 } 291 292 protected LockDiscovery getLockDiscovery(DocumentModel doc, UriInfo uriInfo) { 293 LockDiscovery lockDiscovery = null; 294 if (doc.isLocked()) { 295 String token = backend.getCheckoutUser(doc.getRef()); 296 lockDiscovery = new LockDiscovery(new ActiveLock(LockScope.EXCLUSIVE, LockType.WRITE, Depth.ZERO, 297 new Owner(token), new TimeOut(10000L), new LockToken(new HRef("urn:uuid:" + token)), new LockRoot( 298 new HRef(uriInfo.getRequestUri())))); 299 } 300 return lockDiscovery; 301 } 302 303 protected PropStatBuilderExt getPropStatBuilderExt(DocumentModel doc, UriInfo uriInfo) throws 304 URIException { 305 Date lastModified = getTimePropertyWrapper(doc, "dc:modified"); 306 Date creationDate = getTimePropertyWrapper(doc, "dc:created"); 307 String displayName = URIUtil.encodePath(backend.getDisplayName(doc)); 308 PropStatBuilderExt props = new PropStatBuilderExt(); 309 props.lastModified(lastModified).creationDate(creationDate).displayName(displayName).status(OK); 310 if (doc.isFolder()) { 311 props.isCollection(); 312 } else { 313 String mimeType = "application/octet-stream"; 314 long size = 0; 315 BlobHolder bh = doc.getAdapter(BlobHolder.class); 316 if (bh != null) { 317 Blob blob = bh.getBlob(); 318 if (blob != null) { 319 size = blob.getLength(); 320 mimeType = blob.getMimeType(); 321 } 322 } 323 if (StringUtils.isEmpty(mimeType) || "???".equals(mimeType)) { 324 mimeType = "application/octet-stream"; 325 } 326 props.isResource(size, mimeType); 327 } 328 return props; 329 } 330 331 protected Date getTimePropertyWrapper(DocumentModel doc, String name) { 332 Object property; 333 try { 334 property = doc.getPropertyValue(name); 335 } catch (PropertyNotFoundException e) { 336 property = null; 337 log.debug("Can't get property " + name + " from document " + doc.getId()); 338 } 339 340 if (property != null) { 341 return ((Calendar) property).getTime(); 342 } else { 343 return new Date(); 344 } 345 } 346 347}