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.commons.httpclient.HttpException;
023import org.apache.commons.httpclient.HttpStatus;
024import org.apache.jackrabbit.webdav.DavConstants;
025import org.apache.jackrabbit.webdav.DavException;
026import org.apache.jackrabbit.webdav.MultiStatus;
027import org.apache.jackrabbit.webdav.MultiStatusResponse;
028import org.apache.jackrabbit.webdav.client.methods.MkColMethod;
029import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
030import org.apache.jackrabbit.webdav.property.DavProperty;
031import org.apache.jackrabbit.webdav.property.DavPropertyName;
032import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
033import org.apache.jackrabbit.webdav.property.DavPropertySet;
034import org.apache.maven.wagon.PathUtils;
035import org.apache.maven.wagon.ResourceDoesNotExistException;
036import org.apache.maven.wagon.TransferFailedException;
037import org.apache.maven.wagon.WagonConstants;
038import org.apache.maven.wagon.authorization.AuthorizationException;
039import org.apache.maven.wagon.repository.Repository;
040import org.apache.maven.wagon.shared.http.AbstractHttpClientWagon;
041import org.codehaus.plexus.util.FileUtils;
042import org.codehaus.plexus.util.StringUtils;
043import org.w3c.dom.Node;
044
045import java.io.File;
046import java.io.IOException;
047import java.net.URLDecoder;
048import java.util.ArrayList;
049import java.util.List;
050
051/**
052 * <p>WebDavWagon</p>
053 * <p/>
054 * <p>Allows using a webdav remote repository for downloads and deployments</p>
055 *
056 * @author <a href="mailto:hisidro@exist.com">Henry Isidro</a>
057 * @author <a href="mailto:joakime@apache.org">Joakim Erdfelt</a>
058 * @author <a href="mailto:carlos@apache.org">Carlos Sanchez</a>
059 * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
060 * @plexus.component role="org.apache.maven.wagon.Wagon"
061 * role-hint="dav"
062 * instantiation-strategy="per-lookup"
063 */
064public class WebDavWagon
065    extends AbstractHttpClientWagon
066{
067    protected static final String CONTINUE_ON_FAILURE_PROPERTY = "wagon.webdav.continueOnFailure";
068
069    private final boolean continueOnFailure = Boolean.getBoolean( CONTINUE_ON_FAILURE_PROPERTY );
070
071    /**
072     * Defines the protocol mapping to use.
073     * <p/>
074     * First string is the user definition way to define a webdav url,
075     * the second string is the internal representation of that url.
076     * <p/>
077     * NOTE: The order of the mapping becomes the search order.
078     */
079    private static final String[][] protocolMap =
080        new String[][]{ { "dav:http://", "http://" },    /* maven 2.0.x url string format. (violates URI spec) */
081            { "dav:https://", "https://" },  /* maven 2.0.x url string format. (violates URI spec) */
082            { "dav+http://", "http://" },    /* URI spec compliant (protocol+transport) */
083            { "dav+https://", "https://" },  /* URI spec compliant (protocol+transport) */
084            { "dav://", "http://" },         /* URI spec compliant (protocol only) */
085            { "davs://", "https://" }        /* URI spec compliant (protocol only) */ };
086
087    /**
088     * This wagon supports directory copying
089     *
090     * @return <code>true</code> always
091     */
092    public boolean supportsDirectoryCopy()
093    {
094        return true;
095    }
096
097    /**
098     * Create directories in server as needed.
099     * They are created one at a time until the whole path exists.
100     *
101     * @param dir path to be created in server from repository basedir
102     * @throws IOException
103     * @throws HttpException
104     * @throws TransferFailedException
105     */
106    protected void mkdirs( String dir )
107        throws IOException
108    {
109        Repository repository = getRepository();
110        String basedir = repository.getBasedir();
111
112        String baseUrl = repository.getProtocol() + "://" + repository.getHost();
113        if ( repository.getPort() != WagonConstants.UNKNOWN_PORT )
114        {
115            baseUrl += ":" + repository.getPort();
116        }
117
118        // create relative path that will always have a leading and trailing slash
119        String relpath = FileUtils.normalize( getPath( basedir, dir ) + "/" );
120
121        PathNavigator navigator = new PathNavigator( relpath );
122
123        // traverse backwards until we hit a directory that already exists (OK/NOT_ALLOWED), or that we were able to
124        // create (CREATED), or until we get to the top of the path
125        int status = SC_NULL;
126        do
127        {
128            String url = baseUrl + "/" + navigator.getPath();
129            status = doMkCol( url );
130            if ( status == HttpStatus.SC_OK || status == HttpStatus.SC_CREATED
131                || status == HttpStatus.SC_METHOD_NOT_ALLOWED )
132            {
133                break;
134            }
135        }
136        while ( navigator.backward() );
137
138        // traverse forward creating missing directories
139        while ( navigator.forward() )
140        {
141            String url = baseUrl + "/" + navigator.getPath();
142            status = doMkCol( url );
143            if ( status != HttpStatus.SC_OK && status != HttpStatus.SC_CREATED )
144            {
145                throw new IOException( "Unable to create collection: " + url + "; status code = " + status );
146            }
147        }
148    }
149
150    private int doMkCol( String url )
151        throws IOException
152    {
153        MkColMethod method = null;
154        try
155        {
156            method = new MkColMethod( url );
157            return execute( method );
158        }
159        finally
160        {
161            if ( method != null )
162            {
163                method.releaseConnection();
164            }
165        }
166    }
167
168    /**
169     * Copy a directory from local system to remote webdav server
170     *
171     * @param sourceDirectory      the local directory
172     * @param destinationDirectory the remote destination
173     * @throws TransferFailedException
174     * @throws ResourceDoesNotExistException
175     * @throws AuthorizationException
176     */
177    public void putDirectory( File sourceDirectory, String destinationDirectory )
178        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
179    {
180        File[] listFiles = sourceDirectory.listFiles();
181
182        for ( int i = 0; i < listFiles.length; i++ )
183        {
184            if ( listFiles[i].isDirectory() )
185            {
186                putDirectory( listFiles[i], destinationDirectory + "/" + listFiles[i].getName() );
187            }
188            else
189            {
190                String target = destinationDirectory + "/" + listFiles[i].getName();
191
192                put( listFiles[i], target );
193            }
194        }
195
196    }
197
198    private boolean isDirectory( String url )
199        throws IOException, DavException
200    {
201        DavPropertyNameSet nameSet = new DavPropertyNameSet();
202        nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_RESOURCETYPE ) );
203
204        PropFindMethod method = null;
205        try
206        {
207            method = new PropFindMethod( url, nameSet, DavConstants.DEPTH_0 );
208            execute( method );
209            if ( method.succeeded() )
210            {
211                MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
212                MultiStatusResponse response = multiStatus.getResponses()[0];
213                DavPropertySet propertySet = response.getProperties( HttpStatus.SC_OK );
214                DavProperty property = propertySet.get( DavConstants.PROPERTY_RESOURCETYPE );
215                if ( property != null )
216                {
217                    Node node = (Node) property.getValue();
218                    return node.getLocalName().equals( DavConstants.XML_COLLECTION );
219                }
220            }
221            return false;
222        }
223        finally
224        {
225            if ( method != null )
226            {
227                method.releaseConnection();
228            }
229        }
230    }
231
232    public List<String> getFileList( String destinationDirectory )
233        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
234    {
235        String repositoryUrl = repository.getUrl();
236        String url = repositoryUrl + ( repositoryUrl.endsWith( "/" ) ? "" : "/" ) + destinationDirectory;
237
238        PropFindMethod method = null;
239        try
240        {
241            if ( isDirectory( url ) )
242            {
243                DavPropertyNameSet nameSet = new DavPropertyNameSet();
244                nameSet.add( DavPropertyName.create( DavConstants.PROPERTY_DISPLAYNAME ) );
245
246                method = new PropFindMethod( url, nameSet, DavConstants.DEPTH_1 );
247                int status = execute( method );
248                if ( method.succeeded() )
249                {
250                    ArrayList<String> dirs = new ArrayList<String>();
251                    MultiStatus multiStatus = method.getResponseBodyAsMultiStatus();
252
253                    for ( int i = 0; i < multiStatus.getResponses().length; i++ )
254                    {
255
256                        MultiStatusResponse response = multiStatus.getResponses()[i];
257
258                        String entryUrl = response.getHref();
259                        String fileName = PathUtils.filename( URLDecoder.decode( entryUrl ) );
260                        if ( entryUrl.endsWith( "/" ) )
261                        {
262                            if ( i == 0 )
263                            {
264                                //by design jackrabbit webdav sticks parent directory as the first entry
265                                // so we need to ignore this entry
266                                // http://www.nabble.com/Extra-entry-in-get-file-list-with-jackrabbit-webdav-td21262786.html
267                                // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1
268                                continue;
269                            }
270
271                            //extract "dir/" part of "path.to.dir/"
272                            fileName = PathUtils.filename( PathUtils.dirname( URLDecoder.decode( entryUrl ) ) ) + "/";
273                        }
274
275                        if ( !StringUtils.isEmpty( fileName ) )
276                        {
277                            dirs.add( fileName );
278                        }
279                    }
280                    return dirs;
281                }
282
283                if ( status == HttpStatus.SC_NOT_FOUND )
284                {
285                    throw new ResourceDoesNotExistException( "Destination directory does not exist: " + url );
286                }
287            }
288        }
289        catch ( DavException e )
290        {
291            throw new TransferFailedException( e.getMessage(), e );
292        }
293        catch ( IOException e )
294        {
295            throw new TransferFailedException( e.getMessage(), e );
296        }
297        finally
298        {
299            if ( method != null )
300            {
301                method.releaseConnection();
302            }
303        }
304        throw new ResourceDoesNotExistException(
305            "Destination path exists but is not a " + "WebDAV collection (directory): " + url );
306    }
307
308    public String getURL( Repository repository )
309    {
310        String url = repository.getUrl();
311
312        // Process mappings first.
313        for ( int i = 0; i < protocolMap.length; i++ )
314        {
315            String protocol = protocolMap[i][0];
316            if ( url.startsWith( protocol ) )
317            {
318                return protocolMap[i][1] + url.substring( protocol.length() );
319            }
320        }
321
322        // No mapping trigger? then just return as-is.
323        return url;
324    }
325
326
327    public void put( File source, String resourceName )
328        throws TransferFailedException, ResourceDoesNotExistException, AuthorizationException
329    {
330        try
331        {
332            super.put( source, resourceName );
333        }
334        catch ( TransferFailedException e )
335        {
336            if ( continueOnFailure )
337            {
338                // TODO use a logging mechanism here or a fireTransferWarning
339                System.out.println(
340                    "WARN: Skip unable to transfer '" + resourceName + "' from '" + source.getPath() + "' due to "
341                        + e.getMessage() );
342            }
343            else
344            {
345                throw e;
346            }
347        }
348    }
349}