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.jackrabbit.webdav.DavConstants;
029import org.apache.jackrabbit.webdav.DavException;
030import org.apache.jackrabbit.webdav.MultiStatus;
031import org.apache.jackrabbit.webdav.MultiStatusResponse;
032import org.apache.jackrabbit.webdav.client.methods.HttpMkcol;
033import org.apache.jackrabbit.webdav.client.methods.HttpPropfind;
034import org.apache.jackrabbit.webdav.property.DavProperty;
035import org.apache.jackrabbit.webdav.property.DavPropertyName;
036import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
037import org.apache.jackrabbit.webdav.property.DavPropertySet;
038import org.apache.maven.wagon.PathUtils;
039import org.apache.maven.wagon.ResourceDoesNotExistException;
040import org.apache.maven.wagon.TransferFailedException;
041import org.apache.maven.wagon.WagonConstants;
042import org.apache.maven.wagon.authorization.AuthorizationException;
043import org.apache.maven.wagon.repository.Repository;
044import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon;
045import org.codehaus.plexus.util.FileUtils;
046import org.codehaus.plexus.util.StringUtils;
047import org.w3c.dom.Node;
048
049import java.io.File;
050import java.io.IOException;
051import java.net.URLDecoder;
052import java.util.ArrayList;
053import java.util.List;
054
055/**
056 * <p>WebDavWagon</p>
057 * <p/>
058 * <p>Allows using a WebDAV remote repository for downloads and deployments</p>
059 *
060 * @author <a href="mailto:hisidro@exist.com">Henry Isidro</a>
061 * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a>
062 * @author <a href="mailto:carlos@apache.org">Carlos Sanchez</a>
063 * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
064 * @plexus.component role="org.apache.maven.wagon.Wagon"
065 * role-hint="dav"
066 * instantiation-strategy="per-lookup"
067 */
068public class WebDavWagon
069    extends AbstractHttpClientWagon
070{
071    protected static final String CONTINUE_ON_FAILURE_PROPERTY = "wagon.webdav.continueOnFailure";
072
073    private final boolean continueOnFailure = Boolean.getBoolean( CONTINUE_ON_FAILURE_PROPERTY );
074
075    /**
076     * Defines the protocol mapping to use.
077     * <p/>
078     * First string is the user definition way to define a WebDAV url,
079     * the second string is the internal representation of that url.
080     * <p/>
081     * NOTE: The order of the mapping becomes the search order.
082     */
083    private static final String[][] PROTOCOL_MAP =
084        new String[][]{ { "dav:http://", "http://" },    /* maven 2.0.x url string format. (violates URI spec) */
085            { "dav:https://", "https://" },  /* maven 2.0.x url string format. (violates URI spec) */
086            { "dav+http://", "http://" },    /* URI spec compliant (protocol+transport) */
087            { "dav+https://", "https://" },  /* URI spec compliant (protocol+transport) */
088            { "dav://", "http://" },         /* URI spec compliant (protocol only) */
089            { "davs://", "https://" }        /* URI spec compliant (protocol only) */ };
090
091    /**
092     * This wagon supports directory copying
093     *
094     * @return <code>true</code> always
095     */
096    public boolean supportsDirectoryCopy()
097    {
098        return true;
099    }
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}