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}