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}