001    package org.apache.maven.scm.provider.svn;
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    
022    import org.apache.maven.scm.ScmBranch;
023    import org.apache.maven.scm.ScmTag;
024    import org.apache.maven.scm.ScmVersion;
025    import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
026    import org.codehaus.plexus.util.StringUtils;
027    
028    /**
029     *
030     */
031    public final class SvnTagBranchUtils
032    {
033    
034        private SvnTagBranchUtils() {
035        }
036    
037        public static final String[] REVISION_SPECIFIERS = new String[]{"HEAD", "BASE", "COMMITTED", "PREV"};
038    
039        public static final String SVN_TRUNK = "trunk";
040    
041        public static final String SVN_BRANCHES = "branches";
042    
043        public static final String SVN_TAGS = "tags";
044    
045        public static final String[] SVN_BASE_DIRS = new String[]{SVN_TRUNK, SVN_BRANCHES, SVN_TAGS};
046    
047        /**
048         * Simple helper function to concatenate two paths together with a "/".
049         * Handles trailing / on basePath.
050         * Returns no trailing "/" if the addlPath is null
051         */
052        static String appendPath( String basePath, String addlPath )
053        {
054            basePath = StringUtils.stripEnd( basePath, "/" );
055    
056            if ( StringUtils.isEmpty( addlPath ) )
057            {
058                return basePath;
059            }
060            else
061            {
062                return basePath + "/" + StringUtils.stripStart( addlPath, "/" );
063            }
064        }
065    
066        /**
067         * Returns the project root for the given repository url,
068         * where "project root" is the root of the /trunk, /branches, /tags
069         * directories
070         *
071         * @param repoPath Repository path/url to be searched
072         * @return
073         */
074        public static String getProjectRoot( String repoPath )
075        {
076            for ( int i = 0; i < SVN_BASE_DIRS.length; i++ )
077            {
078                String base = "/" + SVN_BASE_DIRS[i];
079                int pos = repoPath.lastIndexOf( base + "/" );
080                if ( repoPath.endsWith( base ) )
081                {
082                    return repoPath.substring( 0, repoPath.length() - base.length() );
083                }
084                else if ( pos >= 0 )
085                {
086                    return repoPath.substring( 0, pos );
087                }
088            }
089    
090            // At this point we were unable to locate the project root of this url
091            // so assume that the repository url specified is the project root
092            return appendPath( repoPath, null );
093        }
094    
095        public static String resolveTagBase( SvnScmProviderRepository repository )
096        {
097            return resolveTagBase( repository.getUrl() );
098        }
099    
100        public static String resolveTagBase( String repositoryUrl )
101        {
102            return appendPath( getProjectRoot( repositoryUrl ), SVN_TAGS );
103        }
104    
105        public static String resolveBranchBase( SvnScmProviderRepository repository )
106        {
107            return resolveBranchBase( repository.getUrl() );
108        }
109    
110        public static String resolveBranchBase( String repositoryUrl )
111        {
112            return appendPath( getProjectRoot( repositoryUrl ), SVN_BRANCHES );
113        }
114    
115        /**
116         * Resolves a tag to a repository url.
117         * By supplying the repository to this function (rather than calling {@link #resolveTagUrl(String,ScmTag)}
118         * the resolution can use the repository's tagBase to override the default tag location.
119         *
120         * @param repository the repository to use as a base for tag resolution
121         * @param tag        tag name
122         * @return
123         * @see #resolveUrl(String,String,String,ScmBranch)
124         */
125        public static String resolveTagUrl( SvnScmProviderRepository repository, ScmTag tag )
126        {
127            return resolveUrl( repository.getUrl(), repository.getTagBase(), SVN_TAGS, tag );
128        }
129    
130        /**
131         * Resolves a tag to a repository url.
132         * Will not use the {@link SvnScmProviderRepository#getTagBase()} during resolution.
133         *
134         * @param repositoryUrl string url for the repository
135         * @param tag           tag name
136         * @return
137         * @see #resolveUrl(String,String,String,ScmBranch)
138         */
139        public static String resolveTagUrl( String repositoryUrl, ScmTag tag )
140        {
141            return resolveUrl( repositoryUrl, null, SVN_TAGS, tag );
142        }
143    
144        /**
145         * Resolves a branch name to a repository url.
146         * By supplying the repository to this function (rather than calling {@link #resolveBranchUrl(String,ScmBranch)}
147         * the resolution can use the repository's tagBase to override the default tag location.
148         *
149         * @param repository the repository to use as a base for tag resolution
150         * @param branch     tag name
151         * @return
152         * @see #resolveUrl(String,String,String,ScmBranch)
153         */
154        public static String resolveBranchUrl( SvnScmProviderRepository repository, ScmBranch branch )
155        {
156            return resolveUrl( repository.getUrl(), repository.getBranchBase(), SVN_BRANCHES, branch );
157        }
158    
159        /**
160         * Resolves a branch name to a repository url.
161         * Will not use the {@link SvnScmProviderRepository#getTagBase()} during resolution.
162         *
163         * @param repositoryUrl string url for the repository
164         * @param branch        branch name
165         * @return
166         * @see #resolveUrl(String,String,String,ScmBranch)
167         */
168        public static String resolveBranchUrl( String repositoryUrl, ScmBranch branch )
169        {
170            return resolveUrl( repositoryUrl, resolveBranchBase( repositoryUrl ), SVN_BRANCHES, branch );
171        }
172    
173        private static String addSuffix( String baseString, String suffix )
174        {
175            return ( suffix != null ) ? baseString + suffix : baseString;
176        }
177    
178    
179        /**
180         * Resolves a tag or branch name to a repository url.<br>
181         * If the <code>branchTagName</code> is an absolute URL, that value is returned.
182         * (i.e. http://foo.com/svn/myproject/tags/my-tag)<br>
183         * <p/>
184         * If the repository has a {@link SvnScmProviderRepository#getTagBase()} specified,
185         * the tag is simply appended to the tagBase value. Note that at this time, we are using
186         * the tagBase as a base for both branches and tags.<br>
187         * <p/>
188         * If the <code>branchTagName</code> contains a branch/tag specifier (i.e. "/branches", "/tags", "/trunk"),
189         * the <code>branchTagName</code> is appended to the <code>projectRoot</code> without adding the subdir.<br>
190         * Else, the result is in the format of <code>projectRoot/subdir/branchTagName</code> directory.<br>
191         *
192         * @param repositoryUrl string url for the repository
193         * @param tagBase       tagBase to use.
194         * @param subdir        Subdirectory to append to the project root
195         *                      (for branching use "branches", tags use "tags")
196         * @param branchTag     Name of the actual branch or tag. Can be an absolute url, simple tag/branch name,
197         *                      or even contain a relative path to the root like "branches/my-branch"
198         * @return
199         */
200        public static String resolveUrl( String repositoryUrl, String tagBase, String subdir, ScmBranch branchTag )
201        {
202            String branchTagName = branchTag.getName();
203            String projectRoot = getProjectRoot( repositoryUrl );
204            branchTagName = StringUtils.strip( branchTagName, "/" );
205    
206            if ( StringUtils.isEmpty( branchTagName ) )
207            {
208                return null;
209            }
210    
211            // Look for a query string as in ViewCVS urls
212            String queryString = null;
213            if ( repositoryUrl.indexOf( '?' ) >= 0 )
214            {
215                queryString = repositoryUrl.substring( repositoryUrl.indexOf( '?' ) );
216                // if repositoryUrl contains a query string, remove it from repositoryUrlRoot; will be re-appended later
217                projectRoot = StringUtils.replace( projectRoot, queryString, "" );
218            }
219    
220            if ( branchTagName.indexOf( "://" ) >= 0 )
221            {
222                // branch/tag is already an absolute url so just return it.
223                return branchTagName;
224            }
225    
226            // User has a tagBase specified so just return the name appended to the tagBase
227            if ( StringUtils.isNotEmpty( tagBase ) && !tagBase.equals( resolveTagBase( repositoryUrl ) )
228                && !tagBase.equals( resolveBranchBase( repositoryUrl ) ) )
229            {
230                return appendPath( tagBase, branchTagName );
231            }
232    
233            // Look for any "branches/" or "tags/" specifiers in the branchTagName. If one occurs,
234            // don't append the subdir to the projectRoot when appending the name
235            for ( int i = 0; i < SVN_BASE_DIRS.length; i++ )
236            {
237                if ( branchTagName.startsWith( SVN_BASE_DIRS[i] + "/" ) )
238                {
239                    return addSuffix( appendPath( projectRoot, branchTagName ), queryString );
240                }
241            }
242    
243            return addSuffix( appendPath( appendPath( projectRoot, subdir ), branchTagName ), queryString );
244        }
245    
246        /* Helper function that does the checking for {@link #isRevisionSpecifier}
247         */
248        private static boolean checkRevisionArg( String arg )
249        {
250            if ( StringUtils.isNumeric( arg ) || ( arg.startsWith( "{" ) && arg.endsWith( "}" ) ) )
251            {
252                return true;
253            }
254    
255            for ( int i = 0; i < REVISION_SPECIFIERS.length; i++ )
256            {
257                if ( REVISION_SPECIFIERS[i].equalsIgnoreCase( arg ) )
258                {
259                    return true;
260                }
261            }
262    
263            return false;
264        }
265    
266        /**
267         * Returns whether the supplied tag refers to an actual revision or
268         * is specifying a tag/branch url in the repository.
269         * According to the subversion documentation, the following are valid revision specifiers:
270         * NUMBER       revision number
271         * "{" DATE "}" revision at start of the date
272         * "HEAD"       latest in repository
273         * "BASE"       base rev of item's working copy
274         * "COMMITTED"  last commit at or before BASE
275         * "PREV"
276         * <p/>
277         * For command such as diff, the revision argument can be in the format of:
278         * IDENTIFIER:IDENTIFIER   where IDENTIFIER is one of the args listed above
279         */
280        public static boolean isRevisionSpecifier( ScmVersion version )
281        {
282            if ( version == null )
283            {
284                return false;
285            }
286    
287            String versionName = version.getName();
288    
289            if ( StringUtils.isEmpty( versionName ) )
290            {
291                return false;
292            }
293    
294            if ( checkRevisionArg( versionName ) )
295            {
296                return true;
297            }
298    
299            String[] parts = StringUtils.split( versionName, ":" );
300            if ( parts.length == 2 && StringUtils.isNotEmpty( parts[0] ) && StringUtils.isNotEmpty( parts[1] ) )
301            {
302                return checkRevisionArg( parts[0] ) && checkRevisionArg( parts[1] );
303            }
304    
305            return false;
306        }
307    }