View Javadoc
1   package org.apache.maven.wagon.providers.webdav;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.http.HttpException;
23  import org.apache.http.HttpStatus;
24  import org.apache.http.client.methods.CloseableHttpResponse;
25  import org.apache.http.util.EntityUtils;
26  import org.apache.jackrabbit.webdav.DavConstants;
27  import org.apache.jackrabbit.webdav.DavException;
28  import org.apache.jackrabbit.webdav.MultiStatus;
29  import org.apache.jackrabbit.webdav.MultiStatusResponse;
30  import org.apache.jackrabbit.webdav.client.methods.HttpMkcol;
31  import org.apache.jackrabbit.webdav.client.methods.HttpPropfind;
32  import org.apache.jackrabbit.webdav.property.DavProperty;
33  import org.apache.jackrabbit.webdav.property.DavPropertyName;
34  import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
35  import org.apache.jackrabbit.webdav.property.DavPropertySet;
36  import org.apache.maven.wagon.PathUtils;
37  import org.apache.maven.wagon.ResourceDoesNotExistException;
38  import org.apache.maven.wagon.TransferFailedException;
39  import org.apache.maven.wagon.WagonConstants;
40  import org.apache.maven.wagon.authorization.AuthorizationException;
41  import org.apache.maven.wagon.repository.Repository;
42  import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon;
43  import org.codehaus.plexus.util.FileUtils;
44  import org.codehaus.plexus.util.StringUtils;
45  import org.w3c.dom.Node;
46  
47  import java.io.File;
48  import java.io.IOException;
49  import java.net.URLDecoder;
50  import java.util.ArrayList;
51  import java.util.List;
52  
53  import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
54  
55  /**
56   * <p>WebDavWagon</p>
57   * <p/>
58   * <p>Allows using a WebDAV remote repository for downloads and deployments</p>
59   *
60   * @author <a href="mailto:hisidro@exist.com">Henry Isidro</a>
61   * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a>
62   * @author <a href="mailto:carlos@apache.org">Carlos Sanchez</a>
63   * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
64   * @plexus.component role="org.apache.maven.wagon.Wagon"
65   * role-hint="dav"
66   * instantiation-strategy="per-lookup"
67   */
68  public class WebDavWagon
69      extends AbstractHttpClientWagon
70  {
71      protected static final String CONTINUE_ON_FAILURE_PROPERTY = "wagon.webdav.continueOnFailure";
72  
73      private final boolean continueOnFailure = Boolean.getBoolean( CONTINUE_ON_FAILURE_PROPERTY );
74  
75      /**
76       * Defines the protocol mapping to use.
77       * <p/>
78       * First string is the user definition way to define a WebDAV url,
79       * the second string is the internal representation of that url.
80       * <p/>
81       * NOTE: The order of the mapping becomes the search order.
82       */
83      private static final String[][] PROTOCOL_MAP =
84          new String[][]{ { "dav:http://", "http://" },    /* maven 2.0.x url string format. (violates URI spec) */
85              { "dav:https://", "https://" },  /* maven 2.0.x url string format. (violates URI spec) */
86              { "dav+http://", "http://" },    /* URI spec compliant (protocol+transport) */
87              { "dav+https://", "https://" },  /* URI spec compliant (protocol+transport) */
88              { "dav://", "http://" },         /* URI spec compliant (protocol only) */
89              { "davs://", "https://" }        /* URI spec compliant (protocol only) */ };
90  
91      /**
92       * This wagon supports directory copying
93       *
94       * @return <code>true</code> always
95       */
96      public boolean supportsDirectoryCopy()
97      {
98          return true;
99      }
100 
101     /**
102      * Create directories in server as needed.
103      * They are created one at a time until the whole path exists.
104      *
105      * @param dir path to be created in server from repository basedir
106      * @throws IOException
107      * @throws TransferFailedException
108      */
109     protected void mkdirs( String dir )
110         throws IOException
111     {
112         Repository repository = getRepository();
113         String basedir = repository.getBasedir();
114 
115         String baseUrl = repository.getProtocol() + "://" + repository.getHost();
116         if ( repository.getPort() != WagonConstants.UNKNOWN_PORT )
117         {
118             baseUrl += ":" + repository.getPort();
119         }
120 
121         // create relative path that will always have a leading and trailing slash
122         String relpath = FileUtils.normalize( getPath( basedir, dir ) + "/" );
123 
124         PathNavigator navigator = new PathNavigator( relpath );
125 
126         // traverse backwards until we hit a directory that already exists (OK/NOT_ALLOWED), or that we were able to
127         // create (CREATED), or until we get to the top of the path
128         int status = -1;
129         do
130         {
131             String url = baseUrl + "/" + navigator.getPath();
132             status = doMkCol( url );
133             if ( status == HttpStatus.SC_CREATED || status == HttpStatus.SC_METHOD_NOT_ALLOWED )
134             {
135                 break;
136             }
137         }
138         while ( navigator.backward() );
139 
140         // traverse forward creating missing directories
141         while ( navigator.forward() )
142         {
143             String url = baseUrl + "/" + navigator.getPath();
144             status = doMkCol( url );
145             if ( status != HttpStatus.SC_CREATED )
146             {
147                 throw new IOException( "Unable to create collection: " + url + "; status code = " + status );
148             }
149         }
150     }
151 
152     private int doMkCol( String url )
153         throws IOException
154     {
155         HttpMkcol method = new HttpMkcol( url );
156         try ( CloseableHttpResponse closeableHttpResponse = execute( method ) )
157         {
158             return closeableHttpResponse.getStatusLine().getStatusCode();
159         }
160         catch ( HttpException e )
161         {
162             throw new IOException( e.getMessage(), e );
163         }
164         finally
165         {
166             if ( method != null )
167             {
168                 method.releaseConnection();
169             }
170         }
171     }
172 
173     /**
174      * Copy a directory from local system to remote WebDAV server
175      *
176      * @param sourceDirectory      the local directory
177      * @param destinationDirectory the remote destination
178      * @throws TransferFailedException
179      * @throws ResourceDoesNotExistException
180      * @throws AuthorizationException
181      */
182     public void putDirectory( File sourceDirectory, String destinationDirectory )
183         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
184     {
185         for ( File file : sourceDirectory.listFiles() )
186         {
187             if ( file.isDirectory() )
188             {
189                 putDirectory( file, destinationDirectory + "/" + file.getName() );
190             }
191             else
192             {
193                 String target = destinationDirectory + "/" + file.getName();
194 
195                 put( file, target );
196             }
197         }
198     }
199     private boolean isDirectory( String url )
200         throws IOException, DavException
201     {
202         DavPropertyNameSet nameSet = new DavPropertyNameSet();
203         nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_RESOURCETYPE ) );
204 
205         CloseableHttpResponse closeableHttpResponse = null;
206         HttpPropfind method = null;
207         try
208         {
209             method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_0 );
210             closeableHttpResponse = execute( method );
211 
212             if ( method.succeeded( closeableHttpResponse ) )
213             {
214                 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse );
215                 MultiStatusResponse response = multiStatus.getResponses()[0];
216                 DavPropertySet propertySet = response.getProperties( HttpStatus.SC_OK );
217                 DavProperty<?> property = propertySet.get( DavConstants.PROPERTY_RESOURCETYPE );
218                 if ( property != null )
219                 {
220                     Node node = (Node) property.getValue();
221                     return node.getLocalName().equals( DavConstants.XML_COLLECTION );
222                 }
223             }
224             return false;
225         }
226         catch ( HttpException e )
227         {
228             throw new IOException( e.getMessage(), e );
229         }
230         finally
231         {
232             //TODO olamy: not sure we still need this!!
233             if ( method != null )
234             {
235                 method.releaseConnection();
236             }
237             if ( closeableHttpResponse != null )
238             {
239                 closeableHttpResponse.close();
240             }
241         }
242     }
243 
244     public List<String> getFileList( String destinationDirectory )
245         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
246     {
247         String repositoryUrl = repository.getUrl();
248         String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + destinationDirectory;
249 
250         HttpPropfind method = null;
251         CloseableHttpResponse closeableHttpResponse = null;
252         try
253         {
254             if ( isDirectory( url ) )
255             {
256                 DavPropertyNameSet nameSet = new DavPropertyNameSet();
257                 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_DISPLAYNAME ) );
258 
259                 method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_1 );
260                 closeableHttpResponse = execute( method );
261                 if ( method.succeeded( closeableHttpResponse ) )
262                 {
263                     ArrayList<String> dirs = new ArrayList<>();
264                     MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse );
265                     for ( int i = 0; i < multiStatus.getResponses().length; i++ )
266                     {
267                         MultiStatusResponse response = multiStatus.getResponses()[i];
268                         String entryUrl = response.getHref();
269                         String fileName = PathUtils.filename( URLDecoder.decode( entryUrl ) );
270                         if ( entryUrl.endsWith( "/" ) )
271                         {
272                             if ( i == 0 )
273                             {
274                                 // by design jackrabbit WebDAV sticks parent directory as the first entry
275                                 // so we need to ignore this entry
276                                 // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1
277                                 continue;
278                             }
279 
280                             //extract "dir/" part of "path.to.dir/"
281                             fileName = PathUtils.filename( PathUtils.dirname( URLDecoder.decode( entryUrl ) ) ) + "/";
282                         }
283 
284                         if ( !StringUtils.isEmpty( fileName ) )
285                         {
286                             dirs.add( fileName );
287                         }
288                     }
289                     return dirs;
290                 }
291 
292                 int statusCode = closeableHttpResponse.getStatusLine().getStatusCode();
293                 String reasonPhrase = closeableHttpResponse.getStatusLine().getReasonPhrase();
294                 if ( statusCode == HttpStatus.SC_NOT_FOUND || statusCode == HttpStatus.SC_GONE )
295                 {
296                     EntityUtils.consumeQuietly( closeableHttpResponse.getEntity() );
297                     throw new ResourceDoesNotExistException( formatResourceDoesNotExistMessage( url, statusCode,
298                             reasonPhrase, getProxyInfo() ) );
299                 }
300             }
301         }
302         catch ( HttpException e )
303         {
304             throw new TransferFailedException( e.getMessage(), e );
305         }
306         catch ( DavException e )
307         {
308             throw new TransferFailedException( e.getMessage(), e );
309         }
310         catch ( IOException e )
311         {
312             throw new TransferFailedException( e.getMessage(), e );
313         }
314         finally
315         {
316             //TODO olamy: not sure we still need this!!
317             if ( method != null )
318             {
319                 method.releaseConnection();
320             }
321             if ( closeableHttpResponse != null )
322             {
323                 try
324                 {
325                     closeableHttpResponse.close();
326                 }
327                 catch ( IOException e )
328                 {
329                     // ignore
330                 }
331             }
332         }
333         // FIXME WAGON-580; actually the exception is wrong here; we need an IllegalStateException here
334         throw new ResourceDoesNotExistException(
335             "Destination path exists but is not a " + "WebDAV collection (directory): " + url );
336     }
337 
338     public String getURL( Repository repository )
339     {
340         String url = repository.getUrl();
341 
342         // Process mappings first.
343         for ( String[] entry : PROTOCOL_MAP )
344         {
345             String protocol = entry[0];
346             if ( url.startsWith( protocol ) )
347             {
348                 return entry[1] + url.substring( protocol.length() );
349             }
350         }
351 
352         // No mapping trigger? then just return as-is.
353         return url;
354     }
355 
356 
357     public void put( File source, String resourceName )
358         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
359     {
360         try
361         {
362             super.put( source, resourceName );
363         }
364         catch ( TransferFailedException e )
365         {
366             if ( continueOnFailure )
367             {
368                 // TODO use a logging mechanism here or a fireTransferWarning
369                 System.out.println(
370                     "WARN: Skip unable to transfer '" + resourceName + "' from '" + source.getPath() + "' due to "
371                         + e.getMessage() );
372             }
373             else
374             {
375                 throw e;
376             }
377         }
378     }
379 }