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.net.URI; 029import java.net.URISyntaxException; 030import java.util.Calendar; 031import java.util.Date; 032import java.util.HashSet; 033import java.util.Set; 034 035import javax.servlet.http.HttpServletRequest; 036import javax.ws.rs.DELETE; 037import javax.ws.rs.HEAD; 038import javax.ws.rs.HeaderParam; 039import javax.ws.rs.core.Context; 040import javax.ws.rs.core.Response; 041import javax.ws.rs.core.UriInfo; 042 043import net.java.dev.webdav.jaxrs.methods.COPY; 044import net.java.dev.webdav.jaxrs.methods.LOCK; 045import net.java.dev.webdav.jaxrs.methods.MKCOL; 046import net.java.dev.webdav.jaxrs.methods.MOVE; 047import net.java.dev.webdav.jaxrs.methods.PROPPATCH; 048import net.java.dev.webdav.jaxrs.methods.UNLOCK; 049import net.java.dev.webdav.jaxrs.xml.elements.ActiveLock; 050import net.java.dev.webdav.jaxrs.xml.elements.Depth; 051import net.java.dev.webdav.jaxrs.xml.elements.HRef; 052import net.java.dev.webdav.jaxrs.xml.elements.LockRoot; 053import net.java.dev.webdav.jaxrs.xml.elements.LockScope; 054import net.java.dev.webdav.jaxrs.xml.elements.LockToken; 055import net.java.dev.webdav.jaxrs.xml.elements.LockType; 056import net.java.dev.webdav.jaxrs.xml.elements.MultiStatus; 057import net.java.dev.webdav.jaxrs.xml.elements.Owner; 058import net.java.dev.webdav.jaxrs.xml.elements.Prop; 059import net.java.dev.webdav.jaxrs.xml.elements.PropStat; 060import net.java.dev.webdav.jaxrs.xml.elements.Status; 061import net.java.dev.webdav.jaxrs.xml.elements.TimeOut; 062import net.java.dev.webdav.jaxrs.xml.properties.LockDiscovery; 063 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 = new URI(destination).getPath(); 150 } catch (URISyntaxException 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 String codedUrl = "<urn:uuid:" + token + ">"; 262 return Response.ok().entity(prop).header("Lock-Token", codedUrl).build(); 263 } 264 } 265 266 token = backend.lock(doc.getRef()); 267 if (READONLY_TOKEN.equals(token)) { 268 return Response.status(423).build(); 269 } else if (StringUtils.isEmpty(token)) { 270 return Response.status(400).build(); 271 } 272 273 prop = new Prop(getLockDiscovery(doc, uriInfo)); 274 275 backend.saveChanges(); 276 String codedUrl = "<urn:uuid:" + token + ">"; 277 return Response.ok().entity(prop).header("Lock-Token", codedUrl).build(); 278 } 279 280 @UNLOCK 281 public Response unlock() { 282 if (backend.isLocked(doc.getRef())) { 283 if (!backend.canUnlock(doc.getRef())) { 284 return Response.status(423).build(); 285 } else { 286 backend.unlock(doc.getRef()); 287 backend.saveChanges(); 288 return Response.status(204).build(); 289 } 290 } else { 291 // TODO: return an error 292 return Response.status(204).build(); 293 } 294 } 295 296 protected LockDiscovery getLockDiscovery(DocumentModel doc, UriInfo uriInfo) { 297 LockDiscovery lockDiscovery = null; 298 if (doc.isLocked()) { 299 String token = backend.getCheckoutUser(doc.getRef()); 300 String codedUrl = "<urn:uuid:" + token + ">"; 301 lockDiscovery = new LockDiscovery(new ActiveLock(LockScope.EXCLUSIVE, LockType.WRITE, Depth.ZERO, 302 new Owner(token), new TimeOut(10000L), new LockToken(new HRef(codedUrl)), 303 new LockRoot(new HRef(uriInfo.getRequestUri())))); 304 } 305 return lockDiscovery; 306 } 307 308 protected PropStatBuilderExt getPropStatBuilderExt(DocumentModel doc, UriInfo uriInfo) throws 309 URISyntaxException { 310 Date lastModified = getTimePropertyWrapper(doc, "dc:modified"); 311 Date creationDate = getTimePropertyWrapper(doc, "dc:created"); 312 String displayName = new URI(null, backend.getDisplayName(doc), null).toASCIIString(); 313 PropStatBuilderExt props = new PropStatBuilderExt(); 314 props.lastModified(lastModified).creationDate(creationDate).displayName(displayName).status(OK); 315 if (doc.isFolder()) { 316 props.isCollection(); 317 } else { 318 String mimeType = "application/octet-stream"; 319 long size = 0; 320 BlobHolder bh = doc.getAdapter(BlobHolder.class); 321 if (bh != null) { 322 Blob blob = bh.getBlob(); 323 if (blob != null) { 324 size = blob.getLength(); 325 mimeType = blob.getMimeType(); 326 } 327 } 328 if (StringUtils.isEmpty(mimeType) || "???".equals(mimeType)) { 329 mimeType = "application/octet-stream"; 330 } 331 props.isResource(size, mimeType); 332 } 333 return props; 334 } 335 336 protected Date getTimePropertyWrapper(DocumentModel doc, String name) { 337 Object property; 338 try { 339 property = doc.getPropertyValue(name); 340 } catch (PropertyNotFoundException e) { 341 property = null; 342 log.debug("Can't get property " + name + " from document " + doc.getId()); 343 } 344 345 if (property != null) { 346 return ((Calendar) property).getTime(); 347 } else { 348 return new Date(); 349 } 350 } 351 352}