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}