001    package org.apache.archiva.metadata.repository.storage.maven2;
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.archiva.metadata.model.ArtifactMetadata;
023    import org.apache.archiva.metadata.model.maven2.MavenArtifactFacet;
024    import org.apache.archiva.metadata.repository.storage.RepositoryPathTranslator;
025    import org.apache.archiva.common.utils.VersionUtil;
026    import org.slf4j.Logger;
027    import org.slf4j.LoggerFactory;
028    import org.springframework.stereotype.Service;
029    
030    import javax.annotation.PostConstruct;
031    import javax.inject.Inject;
032    import java.io.File;
033    import java.util.List;
034    import java.util.regex.Matcher;
035    import java.util.regex.Pattern;
036    
037    /**
038     *
039     */
040    @Service( "repositoryPathTranslator#maven2" )
041    public class Maven2RepositoryPathTranslator
042        implements RepositoryPathTranslator
043    {
044    
045        private Logger log = LoggerFactory.getLogger( getClass() );
046    
047        private static final char GROUP_SEPARATOR = '.';
048    
049        private static final Pattern TIMESTAMP_PATTERN = Pattern.compile( "([0-9]{8}.[0-9]{6})-([0-9]+).*" );
050        
051    
052        private static final Pattern MAVEN_PLUGIN_PATTERN = Pattern.compile( "^(maven-.*-plugin)|(.*-maven-plugin)$" );    
053    
054        /**
055         *
056         * see #initialize
057         */
058        @Inject
059        private List<ArtifactMappingProvider> artifactMappingProviders;
060    
061        public Maven2RepositoryPathTranslator()
062        {
063            // noop
064        }
065    
066        @PostConstruct
067        public void initialize()
068        {
069            //artifactMappingProviders = new ArrayList<ArtifactMappingProvider>(
070            //    applicationContext.getBeansOfType( ArtifactMappingProvider.class ).values() );
071    
072        }
073    
074    
075        public Maven2RepositoryPathTranslator( List<ArtifactMappingProvider> artifactMappingProviders )
076        {
077            this.artifactMappingProviders = artifactMappingProviders;
078        }
079    
080        public File toFile( File basedir, String namespace, String projectId, String projectVersion, String filename )
081        {
082            return new File( basedir, toPath( namespace, projectId, projectVersion, filename ) );
083        }
084    
085        public File toFile( File basedir, String namespace, String projectId, String projectVersion )
086        {
087            return new File( basedir, toPath( namespace, projectId, projectVersion ) );
088        }
089    
090        public String toPath( String namespace, String projectId, String projectVersion, String filename )
091        {
092            StringBuilder path = new StringBuilder();
093    
094            appendNamespaceToProjectVersion( path, namespace, projectId, projectVersion );
095            path.append( PATH_SEPARATOR );
096            path.append( filename );
097    
098            return path.toString();
099        }
100    
101        private void appendNamespaceToProjectVersion( StringBuilder path, String namespace, String projectId,
102                                                      String projectVersion )
103        {
104            appendNamespaceAndProject( path, namespace, projectId );
105            path.append( projectVersion );
106        }
107    
108        public String toPath( String namespace, String projectId, String projectVersion )
109        {
110            StringBuilder path = new StringBuilder();
111    
112            appendNamespaceToProjectVersion( path, namespace, projectId, projectVersion );
113    
114            return path.toString();
115        }
116    
117        public String toPath( String namespace )
118        {
119            StringBuilder path = new StringBuilder();
120    
121            appendNamespace( path, namespace );
122    
123            return path.toString();
124        }
125    
126        public String toPath( String namespace, String projectId )
127        {
128            StringBuilder path = new StringBuilder();
129    
130            appendNamespaceAndProject( path, namespace, projectId );
131    
132            return path.toString();
133        }
134    
135        private void appendNamespaceAndProject( StringBuilder path, String namespace, String projectId )
136        {
137            appendNamespace( path, namespace );
138            path.append( projectId ).append( PATH_SEPARATOR );
139        }
140    
141        private void appendNamespace( StringBuilder path, String namespace )
142        {
143            path.append( formatAsDirectory( namespace ) ).append( PATH_SEPARATOR );
144        }
145    
146        public File toFile( File basedir, String namespace, String projectId )
147        {
148            return new File( basedir, toPath( namespace, projectId ) );
149        }
150    
151        public File toFile( File basedir, String namespace )
152        {
153            return new File( basedir, toPath( namespace ) );
154        }
155    
156        private String formatAsDirectory( String directory )
157        {
158            return directory.replace( GROUP_SEPARATOR, PATH_SEPARATOR );
159        }
160    
161        public ArtifactMetadata getArtifactForPath( String repoId, String relativePath )
162        {
163            String[] parts = relativePath.replace( '\\', '/' ).split( "/" );
164    
165            int len = parts.length;
166            if ( len < 4 )
167            {
168                throw new IllegalArgumentException(
169                    "Not a valid artifact path in a Maven 2 repository, not enough directories: " + relativePath );
170            }
171    
172            String id = parts[--len];
173            String baseVersion = parts[--len];
174            String artifactId = parts[--len];
175            StringBuilder groupIdBuilder = new StringBuilder();
176            for ( int i = 0; i < len - 1; i++ )
177            {
178                groupIdBuilder.append( parts[i] );
179                groupIdBuilder.append( '.' );
180            }
181            groupIdBuilder.append( parts[len - 1] );
182    
183            return getArtifactFromId( repoId, groupIdBuilder.toString(), artifactId, baseVersion, id );
184        }
185    
186        public ArtifactMetadata getArtifactFromId( String repoId, String namespace, String projectId, String projectVersion,
187                                                   String id )
188        {
189            if ( !id.startsWith( projectId + "-" ) )
190            {
191                throw new IllegalArgumentException( "Not a valid artifact path in a Maven 2 repository, filename '" + id
192                                                        + "' doesn't start with artifact ID '" + projectId + "'" );
193            }
194    
195            MavenArtifactFacet facet = new MavenArtifactFacet();
196    
197            int index = projectId.length() + 1;
198            String version;
199            String idSubStrFromVersion = id.substring( index );
200            if ( idSubStrFromVersion.startsWith( projectVersion ) && !VersionUtil.isUniqueSnapshot( projectVersion ) )
201            {
202                // non-snapshot versions, or non-timestamped snapshot versions
203                version = projectVersion;
204            }
205            else if ( VersionUtil.isGenericSnapshot( projectVersion ) )
206            {
207                // timestamped snapshots
208                try
209                {
210                    int mainVersionLength = projectVersion.length() - 8; // 8 is length of "SNAPSHOT"
211                    if ( mainVersionLength == 0 )
212                    {
213                        throw new IllegalArgumentException(
214                            "Timestamped snapshots must contain the main version, filename was '" + id + "'" );
215                    }
216    
217                    Matcher m = TIMESTAMP_PATTERN.matcher( idSubStrFromVersion.substring( mainVersionLength ) );
218                    m.matches();
219                    String timestamp = m.group( 1 );
220                    String buildNumber = m.group( 2 );
221                    facet.setTimestamp( timestamp );
222                    facet.setBuildNumber( Integer.parseInt( buildNumber ) );
223                    version = idSubStrFromVersion.substring( 0, mainVersionLength ) + timestamp + "-" + buildNumber;
224                }
225                catch ( IllegalStateException e )
226                {
227                    throw new IllegalArgumentException( "Not a valid artifact path in a Maven 2 repository, filename '" + id
228                                                            + "' doesn't contain a timestamped version matching snapshot '"
229                                                            + projectVersion + "'", e);
230                }
231            }
232            else
233            {
234                // invalid
235                throw new IllegalArgumentException(
236                    "Not a valid artifact path in a Maven 2 repository, filename '" + id + "' doesn't contain version '"
237                        + projectVersion + "'" );
238            }
239    
240            String classifier;
241            String ext;
242            index += version.length();
243            if ( index == id.length() )
244            {
245                // no classifier or extension
246                classifier = null;
247                ext = null;
248            }
249            else
250            {
251                char c = id.charAt( index );
252                if ( c == '-' )
253                {
254                    // classifier up until '.'
255                    int extIndex = id.indexOf( '.', index );
256                    if ( extIndex >= 0 )
257                    {
258                        classifier = id.substring( index + 1, extIndex );
259                        ext = id.substring( extIndex + 1 );
260                    }
261                    else
262                    {
263                        classifier = id.substring( index + 1 );
264                        ext = null;
265                    }
266                }
267                else if ( c == '.' )
268                {
269                    // rest is the extension
270                    classifier = null;
271                    ext = id.substring( index + 1 );
272                }
273                else
274                {
275                    throw new IllegalArgumentException( "Not a valid artifact path in a Maven 2 repository, filename '" + id
276                                                            + "' expected classifier or extension but got '"
277                                                            + id.substring( index ) + "'" );
278                }
279            }
280    
281            ArtifactMetadata metadata = new ArtifactMetadata();
282            metadata.setId( id );
283            metadata.setNamespace( namespace );
284            metadata.setProject( projectId );
285            metadata.setRepositoryId( repoId );
286            metadata.setProjectVersion( projectVersion );
287            metadata.setVersion( version );
288    
289            facet.setClassifier( classifier );
290    
291            // we use our own provider here instead of directly accessing Maven's artifact handlers as it has no way
292            // to select the correct order to apply multiple extensions mappings to a preferred type
293            // TODO: this won't allow the user to decide order to apply them if there are conflicts or desired changes -
294            //       perhaps the plugins could register missing entries in configuration, then we just use configuration
295            //       here?
296    
297            String type = null;
298            for ( ArtifactMappingProvider mapping : artifactMappingProviders )
299            {
300                type = mapping.mapClassifierAndExtensionToType( classifier, ext );
301                if ( type != null )
302                {
303                    break;
304                }
305            }
306    
307            // TODO: this is cheating! We should check the POM metadata instead
308            if ( type == null && "jar".equals( ext ) && isArtifactIdValidMavenPlugin( projectId ) )
309            {
310                type = "maven-plugin";
311            }
312    
313            // use extension as default
314            if ( type == null )
315            {
316                type = ext;
317            }
318    
319            // TODO: should we allow this instead?
320            if ( type == null )
321            {
322                throw new IllegalArgumentException(
323                    "Not a valid artifact path in a Maven 2 repository, filename '" + id + "' does not have a type" );
324            }
325    
326            facet.setType( type );
327            metadata.addFacet( facet );
328    
329            return metadata;
330        }
331    
332    
333        public boolean isArtifactIdValidMavenPlugin( String artifactId )
334        {
335            return MAVEN_PLUGIN_PATTERN.matcher( artifactId ).matches();
336        }
337    }