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