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.client.methods.HttpUriRequest;
28  import org.apache.http.impl.auth.BasicScheme;
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.io.InputStream;
53  import java.net.URLDecoder;
54  import java.util.ArrayList;
55  import java.util.List;
56  import java.util.Properties;
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     private static final String DEFAULT_USER_AGENT = getDefaultUserAgent();
105 
106     private static String getDefaultUserAgent()
107     {
108         Properties props = new Properties();
109 
110         try ( InputStream is = AbstractHttpClientWagon.class.getResourceAsStream(
111             "/META-INF/maven/org.apache.maven.wagon/wagon-webdav-jackrabbit/pom.properties" ) )
112         {
113             props.load( is );
114             is.close();
115         }
116         catch ( Exception ignore )
117         {
118             // ignore
119         }
120 
121         String ver = props.getProperty( "version", "unknown-version" );
122         return "Apache-Maven-Wagon/" + ver + " (Java " + System.getProperty( "java.version" ) + "; ";
123     }
124 
125     @Override
126     protected String getUserAgent( HttpUriRequest method )
127     {
128         String userAgent = super.getUserAgent( method );
129         if ( userAgent == null )
130         {
131             return DEFAULT_USER_AGENT;
132         }
133         return userAgent;
134     }
135 
136     /**
137      * Create directories in server as needed.
138      * They are created one at a time until the whole path exists.
139      *
140      * @param dir path to be created in server from repository basedir
141      * @throws IOException
142      * @throws TransferFailedException
143      */
144     protected void mkdirs( String dir )
145         throws IOException
146     {
147         Repository repository = getRepository();
148         String basedir = repository.getBasedir();
149 
150         String baseUrl = repository.getProtocol() + "://" + repository.getHost();
151         if ( repository.getPort() != WagonConstants.UNKNOWN_PORT )
152         {
153             baseUrl += ":" + repository.getPort();
154         }
155 
156         // create relative path that will always have a leading and trailing slash
157         String relpath = FileUtils.normalize( getPath( basedir, dir ) + "/" );
158 
159         PathNavigator navigator = new PathNavigator( relpath );
160 
161         // traverse backwards until we hit a directory that already exists (OK/NOT_ALLOWED), or that we were able to
162         // create (CREATED), or until we get to the top of the path
163         int status = -1;
164         do
165         {
166             String url = baseUrl + "/" + navigator.getPath();
167             status = doMkCol( url );
168             if ( status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED
169                 || status == HttpStatus.SC_METHOD_NOT_ALLOWED )
170             {
171                 break;
172             }
173         }
174         while ( navigator.backward() );
175 
176         // traverse forward creating missing directories
177         while ( navigator.forward() )
178         {
179             String url = baseUrl + "/" + navigator.getPath();
180             status = doMkCol( url );
181             if ( status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED )
182             {
183                 throw new IOException( "Unable to create collection: " + url + "; status code = " + status );
184             }
185         }
186     }
187 
188     private int doMkCol( String url )
189         throws IOException
190     {
191         Repository repo = getRepository();
192         HttpHost targetHost = new HttpHost( repo.getHost(), repo.getPort(), repo.getProtocol() );
193         AuthScope targetScope = getBasicAuthScope().getScope( targetHost );
194 
195         if ( getCredentialsProvider().getCredentials( targetScope ) != null )
196         {
197             BasicScheme targetAuth = new BasicScheme();
198             getAuthCache().put( targetHost, targetAuth );
199         }
200         HttpMkcol method = new HttpMkcol( url );
201         try ( CloseableHttpResponse closeableHttpResponse = execute( method ) )
202         {
203             return closeableHttpResponse.getStatusLine().getStatusCode();
204         }
205         catch ( HttpException e )
206         {
207             throw new IOException( e.getMessage(), e );
208         }
209         finally
210         {
211             if ( method != null )
212             {
213                 method.releaseConnection();
214             }
215         }
216     }
217 
218     /**
219      * Copy a directory from local system to remote WebDAV server
220      *
221      * @param sourceDirectory      the local directory
222      * @param destinationDirectory the remote destination
223      * @throws TransferFailedException
224      * @throws ResourceDoesNotExistException
225      * @throws AuthorizationException
226      */
227     public void putDirectory( File sourceDirectory, String destinationDirectory )
228         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
229     {
230         for ( File file : sourceDirectory.listFiles() )
231         {
232             if ( file.isDirectory() )
233             {
234                 putDirectory( file, destinationDirectory + "/" + file.getName() );
235             }
236             else
237             {
238                 String target = destinationDirectory + "/" + file.getName();
239 
240                 put( file, target );
241             }
242         }
243     }
244     private boolean isDirectory( String url )
245         throws IOException, DavException
246     {
247         DavPropertyNameSet nameSet = new DavPropertyNameSet();
248         nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_RESOURCETYPE ) );
249 
250         CloseableHttpResponse closeableHttpResponse = null;
251         HttpPropfind method = null;
252         try
253         {
254             method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_0 );
255             closeableHttpResponse = execute( method );
256 
257             if ( method.succeeded( closeableHttpResponse ) )
258             {
259                 MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse );
260                 MultiStatusResponse response = multiStatus.getResponses()[0];
261                 DavPropertySet propertySet = response.getProperties( HttpStatus.SC_OK );
262                 DavProperty<?> property = propertySet.get( DavConstants.PROPERTY_RESOURCETYPE );
263                 if ( property != null )
264                 {
265                     Node node = (Node) property.getValue();
266                     return node.getLocalName().equals( DavConstants.XML_COLLECTION );
267                 }
268             }
269             return false;
270         }
271         catch ( HttpException e )
272         {
273             throw new IOException( e.getMessage(), e );
274         }
275         finally
276         {
277             //TODO olamy: not sure we still need this!!
278             if ( method != null )
279             {
280                 method.releaseConnection();
281             }
282             if ( closeableHttpResponse != null )
283             {
284                 closeableHttpResponse.close();
285             }
286         }
287     }
288 
289     public List<String> getFileList( String destinationDirectory )
290         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
291     {
292         String repositoryUrl = repository.getUrl();
293         String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + destinationDirectory;
294 
295         HttpPropfind method = null;
296         CloseableHttpResponse closeableHttpResponse = null;
297         try
298         {
299             if ( isDirectory( url ) )
300             {
301                 DavPropertyNameSet nameSet = new DavPropertyNameSet();
302                 nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_DISPLAYNAME ) );
303 
304                 method = new HttpPropfind( url, nameSet, DavConstants.DEPTH_1 );
305                 closeableHttpResponse = execute( method );
306                 if ( method.succeeded( closeableHttpResponse ) )
307                 {
308                     ArrayList<String> dirs = new ArrayList<>();
309                     MultiStatus multiStatus = method.getResponseBodyAsMultiStatus( closeableHttpResponse );
310                     for ( int i = 0; i < multiStatus.getResponses().length; i++ )
311                     {
312                         MultiStatusResponse response = multiStatus.getResponses()[i];
313                         String entryUrl = response.getHref();
314                         String fileName = PathUtils.filename( URLDecoder.decode( entryUrl ) );
315                         if ( entryUrl.endsWith( "/" ) )
316                         {
317                             if ( i == 0 )
318                             {
319                                 // by design jackrabbit WebDAV sticks parent directory as the first entry
320                                 // so we need to ignore this entry
321                                 // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1
322                                 continue;
323                             }
324 
325                             //extract "dir/" part of "path.to.dir/"
326                             fileName = PathUtils.filename( PathUtils.dirname( URLDecoder.decode( entryUrl ) ) ) + "/";
327                         }
328 
329                         if ( !StringUtils.isEmpty( fileName ) )
330                         {
331                             dirs.add( fileName );
332                         }
333                     }
334                     return dirs;
335                 }
336 
337                 if ( closeableHttpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND )
338                 {
339                     throw new ResourceDoesNotExistException( "Destination directory does not exist: " + url );
340                 }
341             }
342         }
343         catch ( HttpException e )
344         {
345             throw new TransferFailedException( e.getMessage(), e );
346         }
347         catch ( DavException e )
348         {
349             throw new TransferFailedException( e.getMessage(), e );
350         }
351         catch ( IOException e )
352         {
353             throw new TransferFailedException( e.getMessage(), e );
354         }
355         finally
356         {
357             //TODO olamy: not sure we still need this!!
358             if ( method != null )
359             {
360                 method.releaseConnection();
361             }
362             if ( closeableHttpResponse != null )
363             {
364                 try
365                 {
366                     closeableHttpResponse.close();
367                 }
368                 catch ( IOException e )
369                 {
370                     // ignore
371                 }
372             }
373         }
374         throw new ResourceDoesNotExistException(
375             "Destination path exists but is not a " + "WebDAV collection (directory): " + url );
376     }
377 
378     public String getURL( Repository repository )
379     {
380         String url = repository.getUrl();
381 
382         // Process mappings first.
383         for ( String[] entry : PROTOCOL_MAP )
384         {
385             String protocol = entry[0];
386             if ( url.startsWith( protocol ) )
387             {
388                 return entry[1] + url.substring( protocol.length() );
389             }
390         }
391 
392         // No mapping trigger? then just return as-is.
393         return url;
394     }
395 
396 
397     public void put( File source, String resourceName )
398         throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
399     {
400         try
401         {
402             super.put( source, resourceName );
403         }
404         catch ( TransferFailedException e )
405         {
406             if ( continueOnFailure )
407             {
408                 // TODO use a logging mechanism here or a fireTransferWarning
409                 System.out.println(
410                     "WARN: Skip unable to transfer '" + resourceName + "' from '" + source.getPath() + "' due to "
411                         + e.getMessage() );
412             }
413             else
414             {
415                 throw e;
416             }
417         }
418     }
419 }