001package org.apache.maven.wagon.providers.webdav;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.http.HttpException;
023import org.apache.http.HttpHost;
024import org.apache.http.HttpStatus;
025import org.apache.http.auth.AuthScope;
026import org.apache.http.client.methods.CloseableHttpResponse;
027import org.apache.http.impl.auth.BasicScheme;
028import org.apache.http.util.EntityUtils;
029import org.apache.jackrabbit.webdav.DavConstants;
030import org.apache.jackrabbit.webdav.DavException;
031import org.apache.jackrabbit.webdav.MultiStatus;
032import org.apache.jackrabbit.webdav.MultiStatusResponse;
033import org.apache.jackrabbit.webdav.client.methods.HttpMkcol;
034import org.apache.jackrabbit.webdav.client.methods.HttpPropfind;
035import org.apache.jackrabbit.webdav.property.DavProperty;
036import org.apache.jackrabbit.webdav.property.DavPropertyName;
037import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
038import org.apache.jackrabbit.webdav.property.DavPropertySet;
039import org.apache.maven.wagon.PathUtils;
040import org.apache.maven.wagon.ResourceDoesNotExistException;
041import org.apache.maven.wagon.TransferFailedException;
042import org.apache.maven.wagon.WagonConstants;
043import org.apache.maven.wagon.authorization.AuthorizationException;
044import org.apache.maven.wagon.repository.Repository;
045import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon;
046import org.codehaus.plexus.util.FileUtils;
047import org.codehaus.plexus.util.StringUtils;
048import org.w3c.dom.Node;
049
050import java.io.File;
051import java.io.IOException;
052import java.net.URLDecoder;
053import java.util.ArrayList;
054import java.util.List;
055
056import static org.apache.maven.wagon.shared.http.HttpMessageUtils.formatResourceDoesNotExistMessage;
057
058/**
059 * <p>WebDavWagon</p>
060 * <p/>
061 * <p>Allows using a WebDAV remote repository for downloads and deployments</p>
062 *
063 * @author <a href="mailto:hisidro@exist.com">Henry Isidro</a>
064 * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a>
065 * @author <a href="mailto:carlos@apache.org">Carlos Sanchez</a>
066 * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
067 * @plexus.component role="org.apache.maven.wagon.Wagon"
068 * role-hint="dav"
069 * instantiation-strategy="per-lookup"
070 */
071public class WebDavWagon
072    extends AbstractHttpClientWagon
073{
074    protected static final String CONTINUE_ON_FAILURE_PROPERTY = "wagon.webdav.continueOnFailure";
075
076    private final boolean continueOnFailure = Boolean.getBoolean( CONTINUE_ON_FAILURE_PROPERTY );
077
078    /**
079     * Defines the protocol mapping to use.
080     * <p/>
081     * First string is the user definition way to define a WebDAV url,
082     * the second string is the internal representation of that url.
083     * <p/>
084     * NOTE: The order of the mapping becomes the search order.
085     */
086    private static final String[][] PROTOCOL_MAP =
087        new String[][]{ { "dav:http://", "http://" },    /* maven 2.0.x url string format. (violates URI spec) */
088            { "dav:https://", "https://" },  /* maven 2.0.x url string format. (violates URI spec) */
089            { "dav+http://", "http://" },    /* URI spec compliant (protocol+transport) */
090            { "dav+https://", "https://" },  /* URI spec compliant (protocol+transport) */
091            { "dav://", "http://" },         /* URI spec compliant (protocol only) */
092            { "davs://", "https://" }        /* URI spec compliant (protocol only) */ };
093
094    /**
095     * This wagon supports directory copying
096     *
097     * @return <code>true</code> always
098     */
099    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}