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