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.jackrabbit.webdav.DavConstants;
29  import org.apache.jackrabbit.webdav.DavException;
30  import org.apache.jackrabbit.webdav.MultiStatus;
31  import org.apache.jackrabbit.webdav.MultiStatusResponse;
32  import org.apache.jackrabbit.webdav.client.methods.HttpMkcol;
33  import org.apache.jackrabbit.webdav.client.methods.HttpPropfind;
34  import org.apache.jackrabbit.webdav.property.DavProperty;
35  import org.apache.jackrabbit.webdav.property.DavPropertyName;
36  import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
37  import org.apache.jackrabbit.webdav.property.DavPropertySet;
38  import org.apache.maven.wagon.PathUtils;
39  import org.apache.maven.wagon.ResourceDoesNotExistException;
40  import org.apache.maven.wagon.TransferFailedException;
41  import org.apache.maven.wagon.WagonConstants;
42  import org.apache.maven.wagon.authorization.AuthorizationException;
43  import org.apache.maven.wagon.repository.Repository;
44  import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon;
45  import org.codehaus.plexus.util.FileUtils;
46  import org.codehaus.plexus.util.StringUtils;
47  import org.w3c.dom.Node;
48  
49  import java.io.File;
50  import java.io.IOException;
51  import java.net.URLDecoder;
52  import java.util.ArrayList;
53  import java.util.List;
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         Repository repo = getRepository();
156         HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
157         AuthScope targetScope = getBasicAuthScope().getScope( targetHost );
158 
159         if ( getCredentialsProvider().getCredentials( targetScope ) != null )
160         {
161             BasicScheme targetAuth = new BasicScheme();
162             getAuthCache().put( targetHost, targetAuth );
163         }
164         HttpMkcol method = new HttpMkcol( url );
165         try ( CloseableHttpResponse closeableHttpResponse = execute( method ) )
166         {
167             return closeableHttpResponse.getStatusLine().getStatusCode();
168         }
169         catch ( HttpException e )
170         {
171             throw new IOException( e.getMessage(), e );
172         }
173         finally
174         {
175             if ( method != null )
176             {
177                 method.releaseConnection();
178             }
179         }
180     }
181 
182     /**
183      * Copy a directory from local system to remote WebDAV server
184      *
185      * @param sourceDirectory      the local directory
186      * @param destinationDirectory the remote destination
187      * @throws TransferFailedException
188      * @throws ResourceDoesNotExistException
189      * @throws AuthorizationException
190      */
191     public void putDirectory( File sourceDirectory, String destinationDirectory )
192         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
193     {
194         for ( File file : sourceDirectory.listFiles() )
195         {
196             if ( file.isDirectory() )
197             {
198                 putDirectory( file, destinationDirectory + "/" + file.getName() );
199             }
200             else
201             {
202                 String target = destinationDirectory + "/" + file.getName();
203 
204                 put( file, target );
205             }
206         }
207     }
208     private boolean isDirectory( String url )
209         throws IOException, DavException
210     {
211         DavPropertyNameSet nameSet = new DavPropertyNameSet();
212         nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_RESOURCETYPE ) );
213 
214         CloseableHttpResponse closeableHttpResponse = null;
215         HttpPropfind method = null;
216         try
217         {
218             method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_0 );
219             closeableHttpResponse = execute( method );
220 
221             if ( method.succeeded( closeableHttpResponse ) )
222             {
223                 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse );
224                 MultiStatusResponse response = multiStatus.getResponses()[0];
225                 DavPropertySet propertySet = response.getProperties( HttpStatus.SC_OK );
226                 DavProperty<?> property = propertySet.get( DavConstants.PROPERTY_RESOURCETYPE );
227                 if ( property != null )
228                 {
229                     Node node = (Node) property.getValue();
230                     return node.getLocalName().equals( DavConstants.XML_COLLECTION );
231                 }
232             }
233             return false;
234         }
235         catch ( HttpException e )
236         {
237             throw new IOException( e.getMessage(), e );
238         }
239         finally
240         {
241             //TODO olamy: not sure we still need this!!
242             if ( method != null )
243             {
244                 method.releaseConnection();
245             }
246             if ( closeableHttpResponse != null )
247             {
248                 closeableHttpResponse.close();
249             }
250         }
251     }
252 
253     public List<String> getFileList( String destinationDirectory )
254         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
255     {
256         String repositoryUrl = repository.getUrl();
257         String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + destinationDirectory;
258 
259         HttpPropfind method = null;
260         CloseableHttpResponse closeableHttpResponse = null;
261         try
262         {
263             if ( isDirectory( url ) )
264             {
265                 DavPropertyNameSet nameSet = new DavPropertyNameSet();
266                 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_DISPLAYNAME ) );
267 
268                 method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_1 );
269                 closeableHttpResponse = execute( method );
270                 if ( method.succeeded( closeableHttpResponse ) )
271                 {
272                     ArrayList<String> dirs = new ArrayList<>();
273                     MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse );
274                     for ( int i = 0; i < multiStatus.getResponses().length; i++ )
275                     {
276                         MultiStatusResponse response = multiStatus.getResponses()[i];
277                         String entryUrl = response.getHref();
278                         String fileName = PathUtils.filename( URLDecoder.decode( entryUrl ) );
279                         if ( entryUrl.endsWith( "/" ) )
280                         {
281                             if ( i == 0 )
282                             {
283                                 // by design jackrabbit WebDAV sticks parent directory as the first entry
284                                 // so we need to ignore this entry
285                                 // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1
286                                 continue;
287                             }
288 
289                             //extract "dir/" part of "path.to.dir/"
290                             fileName = PathUtils.filename( PathUtils.dirname( URLDecoder.decode( entryUrl ) ) ) + "/";
291                         }
292 
293                         if ( !StringUtils.isEmpty( fileName ) )
294                         {
295                             dirs.add( fileName );
296                         }
297                     }
298                     return dirs;
299                 }
300 
301                 if ( closeableHttpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND )
302                 {
303                     throw new ResourceDoesNotExistException( "Destination directory does not exist: " + url );
304                 }
305             }
306         }
307         catch ( HttpException e )
308         {
309             throw new TransferFailedException( e.getMessage(), e );
310         }
311         catch ( DavException e )
312         {
313             throw new TransferFailedException( e.getMessage(), e );
314         }
315         catch ( IOException e )
316         {
317             throw new TransferFailedException( e.getMessage(), e );
318         }
319         finally
320         {
321             //TODO olamy: not sure we still need this!!
322             if ( method != null )
323             {
324                 method.releaseConnection();
325             }
326             if ( closeableHttpResponse != null )
327             {
328                 try
329                 {
330                     closeableHttpResponse.close();
331                 }
332                 catch ( IOException e )
333                 {
334                     // ignore
335                 }
336             }
337         }
338         throw new ResourceDoesNotExistException(
339             "Destination path exists but is not a " + "WebDAV collection (directory): " + url );
340     }
341 
342     public String getURL( Repository repository )
343     {
344         String url = repository.getUrl();
345 
346         // Process mappings first.
347         for ( String[] entry : PROTOCOL_MAP )
348         {
349             String protocol = entry[0];
350             if ( url.startsWith( protocol ) )
351             {
352                 return entry[1] + url.substring( protocol.length() );
353             }
354         }
355 
356         // No mapping trigger? then just return as-is.
357         return url;
358     }
359 
360 
361     public void put( File source, String resourceName )
362         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
363     {
364         try
365         {
366             super.put( source, resourceName );
367         }
368         catch ( TransferFailedException e )
369         {
370             if ( continueOnFailure )
371             {
372                 // TODO use a logging mechanism here or a fireTransferWarning
373                 System.out.println(
374                     "WARN: Skip unable to transfer '" + resourceName + "' from '" + source.getPath() + "' due to "
375                         + e.getMessage() );
376             }
377             else
378             {
379                 throw e;
380             }
381         }
382     }
383 }