001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider.webdav;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.net.HttpURLConnection;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028
029import org.apache.commons.httpclient.HttpMethod;
030import org.apache.commons.httpclient.HttpMethodBase;
031import org.apache.commons.httpclient.HttpStatus;
032import org.apache.commons.httpclient.URIException;
033import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
034import org.apache.commons.httpclient.methods.RequestEntity;
035import org.apache.commons.httpclient.params.HttpMethodParams;
036import org.apache.commons.httpclient.util.DateUtil;
037import org.apache.commons.vfs2.FileContentInfoFactory;
038import org.apache.commons.vfs2.FileNotFolderException;
039import org.apache.commons.vfs2.FileNotFoundException;
040import org.apache.commons.vfs2.FileObject;
041import org.apache.commons.vfs2.FileSystemException;
042import org.apache.commons.vfs2.FileType;
043import org.apache.commons.vfs2.NameScope;
044import org.apache.commons.vfs2.provider.AbstractFileName;
045import org.apache.commons.vfs2.provider.DefaultFileContent;
046import org.apache.commons.vfs2.provider.URLFileName;
047import org.apache.commons.vfs2.provider.http.HttpFileObject;
048import org.apache.commons.vfs2.util.FileObjectUtils;
049import org.apache.commons.vfs2.util.MonitorOutputStream;
050import org.apache.jackrabbit.webdav.DavConstants;
051import org.apache.jackrabbit.webdav.DavException;
052import org.apache.jackrabbit.webdav.MultiStatus;
053import org.apache.jackrabbit.webdav.MultiStatusResponse;
054import org.apache.jackrabbit.webdav.client.methods.CheckinMethod;
055import org.apache.jackrabbit.webdav.client.methods.CheckoutMethod;
056import org.apache.jackrabbit.webdav.client.methods.DavMethod;
057import org.apache.jackrabbit.webdav.client.methods.DeleteMethod;
058import org.apache.jackrabbit.webdav.client.methods.MkColMethod;
059import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
060import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
061import org.apache.jackrabbit.webdav.client.methods.PropPatchMethod;
062import org.apache.jackrabbit.webdav.client.methods.PutMethod;
063import org.apache.jackrabbit.webdav.client.methods.UncheckoutMethod;
064import org.apache.jackrabbit.webdav.client.methods.VersionControlMethod;
065import org.apache.jackrabbit.webdav.property.DavProperty;
066import org.apache.jackrabbit.webdav.property.DavPropertyName;
067import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
068import org.apache.jackrabbit.webdav.property.DavPropertySet;
069import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
070import org.apache.jackrabbit.webdav.version.DeltaVConstants;
071import org.apache.jackrabbit.webdav.version.VersionControlledResource;
072import org.apache.jackrabbit.webdav.xml.Namespace;
073import org.w3c.dom.Node;
074
075/**
076 * A WebDAV file.
077 *
078 * @since 2.0
079 */
080public class WebdavFileObject extends HttpFileObject<WebdavFileSystem> {
081
082    /**
083     * An OutputStream that writes to a WebDAV resource.
084     * <p>
085     * TODO - Use piped stream to avoid temporary file.
086     * </p>
087     */
088    private class WebdavOutputStream extends MonitorOutputStream {
089        private final WebdavFileObject file;
090
091        WebdavOutputStream(final WebdavFileObject file) {
092            super(new ByteArrayOutputStream());
093            this.file = file;
094        }
095
096        private boolean createVersion(final String urlStr) {
097            try {
098                final VersionControlMethod method = new VersionControlMethod(urlStr);
099                setupMethod(method);
100                execute(method);
101                return true;
102            } catch (final Exception ex) {
103                return false;
104            }
105        }
106
107        /**
108         * Called after this stream is closed.
109         */
110        @Override
111        protected void onClose() throws IOException {
112            final RequestEntity entity = new ByteArrayRequestEntity(((ByteArrayOutputStream) out).toByteArray());
113            final URLFileName fileName = (URLFileName) getName();
114            final String urlStr = toUrlString(fileName);
115            if (builder.isVersioning(getFileSystem().getFileSystemOptions())) {
116                DavPropertySet set = null;
117                boolean fileExists = true;
118                boolean isCheckedIn = true;
119                try {
120                    set = getPropertyNames(fileName);
121                } catch (final FileNotFoundException fnfe) {
122                    fileExists = false;
123                }
124                if (fileExists && set != null) {
125                    if (set.contains(VersionControlledResource.CHECKED_OUT)) {
126                        isCheckedIn = false;
127                    } else if (!set.contains(VersionControlledResource.CHECKED_IN)) {
128                        DavProperty prop = set.get(VersionControlledResource.AUTO_VERSION);
129                        if (prop != null) {
130                            prop = getProperty(fileName, VersionControlledResource.AUTO_VERSION);
131                            if (DeltaVConstants.XML_CHECKOUT_CHECKIN.equals(prop.getValue())) {
132                                createVersion(urlStr);
133                            }
134                        }
135                    }
136                }
137                if (fileExists && isCheckedIn) {
138                    try {
139                        final CheckoutMethod checkout = new CheckoutMethod(urlStr);
140                        setupMethod(checkout);
141                        execute(checkout);
142                        isCheckedIn = false;
143                    } catch (final FileSystemException ex) {
144                        log(ex);
145                    }
146                }
147
148                try {
149                    final PutMethod method = new PutMethod(urlStr);
150                    method.setRequestEntity(entity);
151                    setupMethod(method);
152                    execute(method);
153                    setUserName(fileName, urlStr);
154                } catch (final FileSystemException ex) {
155                    if (!isCheckedIn) {
156                        try {
157                            final UncheckoutMethod method = new UncheckoutMethod(urlStr);
158                            setupMethod(method);
159                            execute(method);
160                            isCheckedIn = true;
161                        } catch (final Exception e) {
162                            // Going to throw original.
163                            log(e);
164                        }
165                        throw ex;
166                    }
167                }
168                if (!fileExists) {
169                    createVersion(urlStr);
170                    try {
171                        final DavPropertySet props = getPropertyNames(fileName);
172                        isCheckedIn = !props.contains(VersionControlledResource.CHECKED_OUT);
173                    } catch (final FileNotFoundException fnfe) {
174                        log(fnfe);
175                    }
176                }
177                if (!isCheckedIn) {
178                    final CheckinMethod checkin = new CheckinMethod(urlStr);
179                    setupMethod(checkin);
180                    execute(checkin);
181                }
182            } else {
183                final PutMethod method = new PutMethod(urlStr);
184                method.setRequestEntity(entity);
185                setupMethod(method);
186                execute(method);
187                try {
188                    setUserName(fileName, urlStr);
189                } catch (final IOException e) {
190                    // Unable to set the user name.
191                    log(e);
192                }
193            }
194            ((DefaultFileContent) file.getContent()).resetAttributes();
195        }
196
197        private void setUserName(final URLFileName fileName, final String urlStr) throws IOException {
198            final List<DefaultDavProperty> list = new ArrayList<>();
199            String name = builder.getCreatorName(getFileSystem().getFileSystemOptions());
200            final String userName = fileName.getUserName();
201            if (name == null) {
202                name = userName;
203            } else if (userName != null) {
204                final String comment = "Modified by user " + userName;
205                list.add(new DefaultDavProperty(DeltaVConstants.COMMENT, comment));
206            }
207            list.add(new DefaultDavProperty(DeltaVConstants.CREATOR_DISPLAYNAME, name));
208            final PropPatchMethod method = new PropPatchMethod(urlStr, list);
209            setupMethod(method);
210            execute(method);
211        }
212    }
213
214    /** The character set property name. */
215    public static final DavPropertyName RESPONSE_CHARSET = DavPropertyName.create("response-charset");
216
217    /**
218     * An empty immutable {@code WebdavFileObject} array.
219     */
220    private static final WebdavFileObject[] EMPTY_ARRAY = {};
221
222    /** The FileSystemConfigBuilder */
223    private final WebdavFileSystemConfigBuilder builder;
224
225    private final WebdavFileSystem fileSystem;
226
227    /**
228     * Constructs a new instance.
229     *
230     * @param fileName the file name.
231     * @param fileSystem the file system.
232     */
233    protected WebdavFileObject(final AbstractFileName fileName, final WebdavFileSystem fileSystem) {
234        super(fileName, fileSystem, WebdavFileSystemConfigBuilder.getInstance());
235        this.fileSystem = fileSystem;
236        builder = (WebdavFileSystemConfigBuilder) WebdavFileSystemConfigBuilder.getInstance();
237    }
238
239    /**
240     * Configures the given HttpMethodBase.
241     *
242     * @param httpMethod The HttpMethodBase to configure.
243     */
244    protected void configureMethod(final HttpMethodBase httpMethod) {
245        httpMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, WebdavMethodRetryHandler.getInstance());
246    }
247
248    /**
249     * Creates this file as a folder.
250     */
251    @Override
252    protected void doCreateFolder() throws Exception {
253        final DavMethod method = new MkColMethod(toUrlString((URLFileName) getName()));
254        setupMethod(method);
255        try {
256            execute(method);
257        } catch (final FileSystemException fse) {
258            throw new FileSystemException("vfs.provider.webdav/create-collection.error", getName(), fse);
259        }
260    }
261
262    /**
263     * Deletes the file.
264     */
265    @Override
266    protected void doDelete() throws Exception {
267        final DavMethod method = new DeleteMethod(toUrlString((URLFileName) getName()));
268        setupMethod(method);
269        execute(method);
270    }
271
272    /**
273     * Returns the properties of the WebDAV resource.
274     */
275    @Override
276    protected Map<String, Object> doGetAttributes() throws Exception {
277        final Map<String, Object> attributes = new HashMap<>();
278        try {
279            final URLFileName fileName = (URLFileName) getName();
280            DavPropertySet properties = getProperties(fileName, DavConstants.PROPFIND_ALL_PROP,
281                    new DavPropertyNameSet(), false);
282            @SuppressWarnings("unchecked") // iterator() is documented to return DavProperty instances
283            final Iterator<DavProperty> iter = properties.iterator();
284            while (iter.hasNext()) {
285                final DavProperty property = iter.next();
286                attributes.put(property.getName().toString(), property.getValue());
287            }
288            properties = getPropertyNames(fileName);
289            @SuppressWarnings("unchecked") // iterator() is documented to return DavProperty instances
290            final Iterator<DavProperty> iter2 = properties.iterator();
291            while (iter2.hasNext()) {
292                DavProperty property = iter2.next();
293                if (!attributes.containsKey(property.getName().getName())) {
294                    property = getProperty(fileName, property.getName());
295                    if (property != null) {
296                        final Object name = property.getName();
297                        final Object value = property.getValue();
298                        if (name != null && value != null) {
299                            attributes.put(name.toString(), value);
300                        }
301                    }
302                }
303            }
304            return attributes;
305        } catch (final Exception e) {
306            throw new FileSystemException("vfs.provider.webdav/get-attributes.error", getName(), e);
307        }
308    }
309
310    /**
311     * Returns the size of the file content (in bytes).
312     */
313    @Override
314    protected long doGetContentSize() throws Exception {
315        final DavProperty property = getProperty((URLFileName) getName(), DavConstants.PROPERTY_GETCONTENTLENGTH);
316        if (property != null) {
317            final String value = (String) property.getValue();
318            return Long.parseLong(value);
319        }
320        return 0;
321    }
322
323    /**
324     * Returns the last modified time of this file. Is only called if {@link #doGetType} does not return
325     * {@link FileType#IMAGINARY}.
326     */
327    @Override
328    protected long doGetLastModifiedTime() throws Exception {
329        final DavProperty property = getProperty((URLFileName) getName(), DavConstants.PROPERTY_GETLASTMODIFIED);
330        if (property != null) {
331            final String value = (String) property.getValue();
332            return DateUtil.parseDate(value).getTime();
333        }
334        return 0;
335    }
336
337    @Override
338    protected OutputStream doGetOutputStream(final boolean bAppend) throws Exception {
339        return new WebdavOutputStream(this);
340    }
341
342    /**
343     * Determines the type of this file. Must not return null. The return value of this method is cached, so the
344     * implementation can be expensive.
345     */
346    @Override
347    protected FileType doGetType() throws Exception {
348        try {
349            return isDirectory((URLFileName) getName()) ? FileType.FOLDER : FileType.FILE;
350        } catch (final FileNotFolderException | FileNotFoundException fnfe) {
351            return FileType.IMAGINARY;
352        }
353
354    }
355
356    /**
357     * Determines if this file can be written to. Is only called if {@link #doGetType} does not return
358     * {@link FileType#IMAGINARY}.
359     * <p>
360     * This implementation always returns true.
361     *
362     * @return true if the file is writable.
363     * @throws Exception if an error occurs.
364     */
365    @Override
366    protected boolean doIsWriteable() throws Exception {
367        return true;
368    }
369
370    /**
371     * Lists the children of the file.
372     */
373    @Override
374    protected String[] doListChildren() throws Exception {
375        // use doListChildrenResolved for performance
376        return null;
377    }
378
379    /**
380     * Lists the children of the file.
381     */
382    @Override
383    protected FileObject[] doListChildrenResolved() throws Exception {
384        PropFindMethod method = null;
385        try {
386            final URLFileName name = (URLFileName) getName();
387            if (isDirectory(name)) {
388                final DavPropertyNameSet nameSet = new DavPropertyNameSet();
389                nameSet.add(DavPropertyName.create(DavConstants.PROPERTY_DISPLAYNAME));
390
391                method = new PropFindMethod(toUrlString(name), nameSet, DavConstants.DEPTH_1);
392
393                execute(method);
394                final List<WebdavFileObject> vfs = new ArrayList<>();
395                if (method.succeeded()) {
396                    final MultiStatusResponse[] responses = method.getResponseBodyAsMultiStatus().getResponses();
397
398                    for (final MultiStatusResponse response : responses) {
399                        if (isCurrentFile(response.getHref(), name)) {
400                            continue;
401                        }
402                        final String resourceName = resourceName(response.getHref());
403                        if (!resourceName.isEmpty()) {
404                            final WebdavFileObject fo = (WebdavFileObject) FileObjectUtils.getAbstractFileObject(
405                                    getFileSystem().resolveFile(getFileSystem().getFileSystemManager()
406                                            .resolveName(getName(), resourceName, NameScope.CHILD)));
407                            vfs.add(fo);
408                        }
409                    }
410                }
411                return vfs.toArray(EMPTY_ARRAY);
412            }
413            throw new FileNotFolderException(getName());
414        } catch (final FileNotFolderException fnfe) {
415            throw fnfe;
416        } catch (final DavException | IOException e) {
417            throw new FileSystemException(e.getMessage(), e);
418        } finally {
419            if (method != null) {
420                method.releaseConnection();
421            }
422        }
423    }
424
425    /**
426     * Rename the file.
427     */
428    @Override
429    protected void doRename(final FileObject newFile) throws Exception {
430        final String url = encodePath(toUrlString((URLFileName) getName()));
431        final String dest = toUrlString((URLFileName) newFile.getName(), false);
432        final DavMethod method = new MoveMethod(url, dest, false);
433        setupMethod(method);
434        execute(method);
435    }
436
437    /**
438     * Sets an attribute of this file. Is only called if {@link #doGetType} does not return {@link FileType#IMAGINARY}.
439     */
440    @Override
441    protected void doSetAttribute(final String attrName, final Object value) throws Exception {
442        try {
443            final URLFileName fileName = (URLFileName) getName();
444            final String urlStr = toUrlString(fileName);
445            final DavPropertySet properties = new DavPropertySet();
446            final DavPropertyNameSet propertyNameSet = new DavPropertyNameSet();
447            final DavProperty property = new DefaultDavProperty(attrName, value, Namespace.EMPTY_NAMESPACE);
448            if (value != null) {
449                properties.add(property);
450            } else {
451                propertyNameSet.add(property.getName()); // remove property
452            }
453
454            final PropPatchMethod method = new PropPatchMethod(urlStr, properties, propertyNameSet);
455            setupMethod(method);
456            execute(method);
457            if (!method.succeeded()) {
458                throw new FileSystemException("Property '" + attrName + "' could not be set.");
459            }
460        } catch (final FileSystemException fse) {
461            throw fse;
462        } catch (final Exception e) {
463            throw new FileSystemException("vfs.provider.webdav/set-attributes", e, getName(), attrName);
464        }
465    }
466
467    /**
468     * Execute a 'Workspace' operation.
469     *
470     * @param method The DavMethod to invoke.
471     * @throws FileSystemException If an error occurs.
472     */
473    private void execute(final DavMethod method) throws FileSystemException {
474        try {
475            final int status = fileSystem.getClient().executeMethod(method);
476            if (status == HttpURLConnection.HTTP_NOT_FOUND || status == HttpURLConnection.HTTP_GONE) {
477                throw new FileNotFoundException(method.getURI());
478            }
479            method.checkSuccess();
480        } catch (final FileSystemException fse) {
481            throw fse;
482        } catch (final IOException e) {
483            throw new FileSystemException(e);
484        } catch (final DavException e) {
485            throw ExceptionConverter.generate(e);
486        } finally {
487            if (method != null) {
488                method.releaseConnection();
489            }
490        }
491    }
492
493    @Override
494    protected FileContentInfoFactory getFileContentInfoFactory() {
495        return new WebdavFileContentInfoFactory();
496    }
497
498    DavPropertySet getProperties(final URLFileName name) throws FileSystemException {
499        return getProperties(name, DavConstants.PROPFIND_ALL_PROP, new DavPropertyNameSet(), false);
500    }
501
502    DavPropertySet getProperties(final URLFileName name, final DavPropertyNameSet nameSet, final boolean addEncoding)
503            throws FileSystemException {
504        return getProperties(name, DavConstants.PROPFIND_BY_PROPERTY, nameSet, addEncoding);
505    }
506
507    DavPropertySet getProperties(final URLFileName name, final int type, final DavPropertyNameSet nameSet,
508            final boolean addEncoding) throws FileSystemException {
509        try {
510            final String urlStr = toUrlString(name);
511            final PropFindMethod method = new PropFindMethod(urlStr, type, nameSet, DavConstants.DEPTH_0);
512            setupMethod(method);
513            execute(method);
514            if (method.succeeded()) {
515                final MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
516                final MultiStatusResponse response = multiStatus.getResponses()[0];
517                final DavPropertySet props = response.getProperties(HttpStatus.SC_OK);
518                if (addEncoding) {
519                    final DavProperty prop = new DefaultDavProperty(RESPONSE_CHARSET, method.getResponseCharSet());
520                    props.add(prop);
521                }
522                return props;
523            }
524            return new DavPropertySet();
525        } catch (final FileSystemException fse) {
526            throw fse;
527        } catch (final Exception e) {
528            throw new FileSystemException("vfs.provider.webdav/get-property.error", e, getName(), name, type,
529                    nameSet.getContent(), addEncoding);
530        }
531    }
532
533    DavProperty getProperty(final URLFileName fileName, final DavPropertyName name) throws FileSystemException {
534        final DavPropertyNameSet nameSet = new DavPropertyNameSet();
535        nameSet.add(name);
536        final DavPropertySet propertySet = getProperties(fileName, nameSet, false);
537        return propertySet.get(name);
538    }
539
540    DavProperty getProperty(final URLFileName fileName, final String property) throws FileSystemException {
541        return getProperty(fileName, DavPropertyName.create(property));
542    }
543
544    DavPropertySet getPropertyNames(final URLFileName name) throws FileSystemException {
545        return getProperties(name, DavConstants.PROPFIND_PROPERTY_NAMES, new DavPropertyNameSet(), false);
546    }
547
548    /**
549     * Convert the FileName to an encoded url String.
550     *
551     * @param name The FileName.
552     * @return The encoded URL String.
553     */
554    private String hrefString(final URLFileName name) {
555        final URLFileName newFile = new URLFileName("http", name.getHostName(), name.getPort(), name.getDefaultPort(),
556                null, null, name.getPath(), name.getType(), name.getQueryString());
557        try {
558            return newFile.getURIEncoded(getUrlCharset());
559        } catch (final Exception e) {
560            return name.getURI();
561        }
562    }
563
564    private boolean isCurrentFile(final String href, final URLFileName fileName) {
565        String name = hrefString(fileName);
566        if (href.endsWith("/") && !name.endsWith("/")) {
567            name += "/";
568        }
569        return href.equals(name) || href.equals(fileName.getPath());
570    }
571
572    private boolean isDirectory(final URLFileName name) throws IOException {
573        try {
574            final DavProperty property = getProperty(name, DavConstants.PROPERTY_RESOURCETYPE);
575            final Node node;
576            if (property != null && (node = (Node) property.getValue()) != null) {
577                return node.getLocalName().equals(DavConstants.XML_COLLECTION);
578            }
579            return false;
580        } catch (final FileNotFoundException fse) {
581            throw new FileNotFolderException(name);
582        }
583    }
584
585    void log(final Exception ex) {
586        // TODO Consider logging.
587    }
588
589    /**
590     * Returns the resource name from the path.
591     *
592     * @param path the path to the file.
593     * @return The resource name
594     */
595    private String resourceName(String path) {
596        if (path.endsWith("/")) {
597            path = path.substring(0, path.length() - 1);
598        }
599        final int i = path.lastIndexOf("/");
600        return i >= 0 ? path.substring(i + 1) : path;
601    }
602
603    /**
604     * Prepares a Method object.
605     *
606     * @param method the HttpMethod.
607     * @throws FileSystemException if an error occurs encoding the uri.
608     * @throws URIException if the URI is in error.
609     */
610    @Override
611    protected void setupMethod(final HttpMethod method) throws FileSystemException, URIException {
612        final String pathEncoded = ((URLFileName) getName()).getPathQueryEncoded(getUrlCharset());
613        method.setPath(pathEncoded);
614        method.setFollowRedirects(getFollowRedirect());
615        method.setRequestHeader("User-Agent", "Jakarta-Commons-VFS");
616        method.addRequestHeader("Cache-control", "no-cache");
617        method.addRequestHeader("Cache-store", "no-store");
618        method.addRequestHeader("Pragma", "no-cache");
619        method.addRequestHeader("Expires", "0");
620    }
621
622    private String toUrlString(final URLFileName name) {
623        return toUrlString(name, true);
624    }
625
626    /**
627     * Converts the given URLFileName to an encoded URL String.
628     *
629     * @param name The FileName.
630     * @param includeUserInfo true if user information should be included.
631     * @return The encoded URL String.
632     */
633    private String toUrlString(final URLFileName name, final boolean includeUserInfo) {
634        String user = null;
635        String password = null;
636        if (includeUserInfo) {
637            user = name.getUserName();
638            password = name.getPassword();
639        }
640        final URLFileName newFile = new URLFileName("http", name.getHostName(), name.getPort(), name.getDefaultPort(),
641                user, password, name.getPath(), name.getType(), name.getQueryString());
642        try {
643            return newFile.getURIEncoded(getUrlCharset());
644        } catch (final Exception e) {
645            return name.getURI();
646        }
647    }
648}