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