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 OutputStream that writes to a WebDAV resource.
84       * <p>
85       * TODO - Use piped stream to avoid temporary file.
86       * </p>
87       */
88      private class WebdavOutputStream extends MonitorOutputStream {
89          private final WebdavFileObject file;
90  
91          WebdavOutputStream(final WebdavFileObject file) {
92              super(new ByteArrayOutputStream());
93              this.file = file;
94          }
95  
96          private boolean createVersion(final String urlStr) {
97              try {
98                  final VersionControlMethod method = new VersionControlMethod(urlStr);
99                  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 }