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