001    package org.apache.maven.repository.internal;
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 java.io.File;
023    import java.io.FileInputStream;
024    import java.io.IOException;
025    import java.util.ArrayList;
026    import java.util.Collections;
027    import java.util.HashMap;
028    import java.util.List;
029    import java.util.Map;
030    
031    import org.apache.maven.artifact.repository.metadata.Snapshot;
032    import org.apache.maven.artifact.repository.metadata.SnapshotVersion;
033    import org.apache.maven.artifact.repository.metadata.Versioning;
034    import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
035    import org.codehaus.plexus.component.annotations.Component;
036    import org.codehaus.plexus.component.annotations.Requirement;
037    import org.codehaus.plexus.util.IOUtil;
038    import org.codehaus.plexus.util.StringUtils;
039    import org.sonatype.aether.ConfigurationProperties;
040    import org.sonatype.aether.RepositoryCache;
041    import org.sonatype.aether.RequestTrace;
042    import org.sonatype.aether.RepositoryEvent.EventType;
043    import org.sonatype.aether.RepositorySystemSession;
044    import org.sonatype.aether.SyncContext;
045    import org.sonatype.aether.util.DefaultRequestTrace;
046    import org.sonatype.aether.util.listener.DefaultRepositoryEvent;
047    import org.sonatype.aether.util.metadata.DefaultMetadata;
048    import org.sonatype.aether.artifact.Artifact;
049    import org.sonatype.aether.impl.MetadataResolver;
050    import org.sonatype.aether.impl.RepositoryEventDispatcher;
051    import org.sonatype.aether.impl.SyncContextFactory;
052    import org.sonatype.aether.impl.VersionResolver;
053    import org.sonatype.aether.impl.internal.CacheUtils;
054    import org.sonatype.aether.metadata.Metadata;
055    import org.sonatype.aether.repository.ArtifactRepository;
056    import org.sonatype.aether.repository.LocalRepository;
057    import org.sonatype.aether.repository.RemoteRepository;
058    import org.sonatype.aether.repository.WorkspaceReader;
059    import org.sonatype.aether.repository.WorkspaceRepository;
060    import org.sonatype.aether.resolution.MetadataRequest;
061    import org.sonatype.aether.resolution.MetadataResult;
062    import org.sonatype.aether.resolution.VersionRequest;
063    import org.sonatype.aether.resolution.VersionResolutionException;
064    import org.sonatype.aether.resolution.VersionResult;
065    import org.sonatype.aether.spi.locator.Service;
066    import org.sonatype.aether.spi.locator.ServiceLocator;
067    import org.sonatype.aether.spi.log.Logger;
068    import org.sonatype.aether.spi.log.NullLogger;
069    
070    /**
071     * @author Benjamin Bentmann
072     */
073    @Component( role = VersionResolver.class )
074    public class DefaultVersionResolver
075        implements VersionResolver, Service
076    {
077    
078        private static final String MAVEN_METADATA_XML = "maven-metadata.xml";
079    
080        private static final String RELEASE = "RELEASE";
081    
082        private static final String LATEST = "LATEST";
083    
084        private static final String SNAPSHOT = "SNAPSHOT";
085    
086        @SuppressWarnings( "unused" )
087        @Requirement
088        private Logger logger = NullLogger.INSTANCE;
089    
090        @Requirement
091        private MetadataResolver metadataResolver;
092    
093        @Requirement
094        private SyncContextFactory syncContextFactory;
095    
096        @Requirement
097        private RepositoryEventDispatcher repositoryEventDispatcher;
098    
099        public void initService( ServiceLocator locator )
100        {
101            setLogger( locator.getService( Logger.class ) );
102            setMetadataResolver( locator.getService( MetadataResolver.class ) );
103            setSyncContextFactory( locator.getService( SyncContextFactory.class ) );
104            setRepositoryEventDispatcher( locator.getService( RepositoryEventDispatcher.class ) );
105        }
106    
107        public DefaultVersionResolver setLogger( Logger logger )
108        {
109            this.logger = ( logger != null ) ? logger : NullLogger.INSTANCE;
110            return this;
111        }
112    
113        public DefaultVersionResolver setMetadataResolver( MetadataResolver metadataResolver )
114        {
115            if ( metadataResolver == null )
116            {
117                throw new IllegalArgumentException( "metadata resolver has not been specified" );
118            }
119            this.metadataResolver = metadataResolver;
120            return this;
121        }
122    
123        public DefaultVersionResolver setSyncContextFactory( SyncContextFactory syncContextFactory )
124        {
125            if ( syncContextFactory == null )
126            {
127                throw new IllegalArgumentException( "sync context factory has not been specified" );
128            }
129            this.syncContextFactory = syncContextFactory;
130            return this;
131        }
132    
133        public DefaultVersionResolver setRepositoryEventDispatcher( RepositoryEventDispatcher repositoryEventDispatcher )
134        {
135            if ( repositoryEventDispatcher == null )
136            {
137                throw new IllegalArgumentException( "repository event dispatcher has not been specified" );
138            }
139            this.repositoryEventDispatcher = repositoryEventDispatcher;
140            return this;
141        }
142    
143        public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
144            throws VersionResolutionException
145        {
146            RequestTrace trace = DefaultRequestTrace.newChild( request.getTrace(), request );
147    
148            Artifact artifact = request.getArtifact();
149    
150            String version = artifact.getVersion();
151    
152            VersionResult result = new VersionResult( request );
153    
154            Key cacheKey = null;
155            RepositoryCache cache = session.getCache();
156            if ( cache != null && !ConfigurationProperties.get( session, "aether.versionResolver.noCache", false ) )
157            {
158                cacheKey = new Key( session, request );
159    
160                Object obj = cache.get( session, cacheKey );
161                if ( obj instanceof Record )
162                {
163                    Record record = (Record) obj;
164                    result.setVersion( record.version );
165                    result.setRepository( CacheUtils.getRepository( session, request.getRepositories(), record.repoClass,
166                                                                    record.repoId ) );
167                    return result;
168                }
169            }
170    
171            Metadata metadata;
172    
173            if ( RELEASE.equals( version ) )
174            {
175                metadata =
176                    new DefaultMetadata( artifact.getGroupId(), artifact.getArtifactId(), MAVEN_METADATA_XML,
177                                         Metadata.Nature.RELEASE );
178            }
179            else if ( LATEST.equals( version ) )
180            {
181                metadata =
182                    new DefaultMetadata( artifact.getGroupId(), artifact.getArtifactId(), MAVEN_METADATA_XML,
183                                         Metadata.Nature.RELEASE_OR_SNAPSHOT );
184            }
185            else if ( version.endsWith( SNAPSHOT ) )
186            {
187                WorkspaceReader workspace = session.getWorkspaceReader();
188                if ( workspace != null && workspace.findVersions( artifact ).contains( version ) )
189                {
190                    metadata = null;
191                    result.setRepository( workspace.getRepository() );
192                }
193                else
194                {
195                    metadata =
196                        new DefaultMetadata( artifact.getGroupId(), artifact.getArtifactId(), version, MAVEN_METADATA_XML,
197                                             Metadata.Nature.SNAPSHOT );
198                }
199            }
200            else
201            {
202                metadata = null;
203            }
204    
205            if ( metadata == null )
206            {
207                result.setVersion( version );
208            }
209            else
210            {
211                List<MetadataRequest> metadataRequests = new ArrayList<MetadataRequest>( request.getRepositories().size() );
212    
213                metadataRequests.add( new MetadataRequest( metadata, null, request.getRequestContext() ) );
214    
215                for ( RemoteRepository repository : request.getRepositories() )
216                {
217                    MetadataRequest metadataRequest =
218                        new MetadataRequest( metadata, repository, request.getRequestContext() );
219                    metadataRequest.setDeleteLocalCopyIfMissing( true );
220                    metadataRequest.setFavorLocalRepository( true );
221                    metadataRequest.setTrace( trace );
222                    metadataRequests.add( metadataRequest );
223                }
224    
225                List<MetadataResult> metadataResults = metadataResolver.resolveMetadata( session, metadataRequests );
226    
227                Map<String, VersionInfo> infos = new HashMap<String, VersionInfo>();
228    
229                for ( MetadataResult metadataResult : metadataResults )
230                {
231                    result.addException( metadataResult.getException() );
232    
233                    ArtifactRepository repository = metadataResult.getRequest().getRepository();
234                    if ( repository == null )
235                    {
236                        repository = session.getLocalRepository();
237                    }
238    
239                    Versioning versioning = readVersions( session, trace, metadataResult.getMetadata(), repository, result );
240                    merge( artifact, infos, versioning, repository );
241                }
242    
243                if ( RELEASE.equals( version ) )
244                {
245                    resolve( result, infos, RELEASE );
246                }
247                else if ( LATEST.equals( version ) )
248                {
249                    if ( !resolve( result, infos, LATEST ) )
250                    {
251                        resolve( result, infos, RELEASE );
252                    }
253    
254                    if ( result.getVersion() != null && result.getVersion().endsWith( SNAPSHOT ) )
255                    {
256                        VersionRequest subRequest = new VersionRequest();
257                        subRequest.setArtifact( artifact.setVersion( result.getVersion() ) );
258                        if ( result.getRepository() instanceof RemoteRepository )
259                        {
260                            subRequest.setRepositories( Collections.singletonList( (RemoteRepository) result.getRepository() ) );
261                        }
262                        else
263                        {
264                            subRequest.setRepositories( request.getRepositories() );
265                        }
266                        VersionResult subResult = resolveVersion( session, subRequest );
267                        result.setVersion( subResult.getVersion() );
268                        result.setRepository( subResult.getRepository() );
269                        for ( Exception exception : subResult.getExceptions() )
270                        {
271                            result.addException( exception );
272                        }
273                    }
274                }
275                else
276                {
277                    String key = SNAPSHOT + getKey( artifact.getClassifier(), artifact.getExtension() );
278                    merge( infos, SNAPSHOT, key );
279                    if ( !resolve( result, infos, key ) )
280                    {
281                        result.setVersion( version );
282                    }
283                }
284    
285                if ( StringUtils.isEmpty( result.getVersion() ) )
286                {
287                    throw new VersionResolutionException( result );
288                }
289            }
290    
291            if ( cacheKey != null && metadata != null && isSafelyCacheable( session, artifact ) )
292            {
293                cache.put( session, cacheKey, new Record( result.getVersion(), result.getRepository() ) );
294            }
295    
296            return result;
297        }
298    
299        private boolean resolve( VersionResult result, Map<String, VersionInfo> infos, String key )
300        {
301            VersionInfo info = infos.get( key );
302            if ( info != null )
303            {
304                result.setVersion( info.version );
305                result.setRepository( info.repository );
306            }
307            return info != null;
308        }
309    
310        private Versioning readVersions( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
311                                         ArtifactRepository repository, VersionResult result )
312        {
313            Versioning versioning = null;
314    
315            FileInputStream fis = null;
316            try
317            {
318                if ( metadata != null )
319                {
320                    SyncContext syncContext = syncContextFactory.newInstance( session, true );
321    
322                    try
323                    {
324                        syncContext.acquire( null, Collections.singleton( metadata ) );
325    
326                        if ( metadata.getFile() != null && metadata.getFile().exists() )
327                        {
328                            fis = new FileInputStream( metadata.getFile() );
329                            org.apache.maven.artifact.repository.metadata.Metadata m =
330                                new MetadataXpp3Reader().read( fis, false );
331                            versioning = m.getVersioning();
332    
333                            /*
334                             * NOTE: Users occasionally misuse the id "local" for remote repos which screws up the metadata
335                             * of the local repository. This is especially troublesome during snapshot resolution so we try
336                             * to handle that gracefully.
337                             */
338                            if ( versioning != null && repository instanceof LocalRepository )
339                            {
340                                if ( versioning.getSnapshot() != null && versioning.getSnapshot().getBuildNumber() > 0 )
341                                {
342                                    Versioning repaired = new Versioning();
343                                    repaired.setLastUpdated( versioning.getLastUpdated() );
344                                    Snapshot snapshot = new Snapshot();
345                                    snapshot.setLocalCopy( true );
346                                    repaired.setSnapshot( snapshot );
347                                    versioning = repaired;
348    
349                                    throw new IOException( "Snapshot information corrupted with remote repository data"
350                                        + ", please verify that no remote repository uses the id '" + repository.getId()
351                                        + "'" );
352                                }
353                            }
354                        }
355                    }
356                    finally
357                    {
358                        syncContext.release();
359                    }
360                }
361            }
362            catch ( Exception e )
363            {
364                invalidMetadata( session, trace, metadata, repository, e );
365                result.addException( e );
366            }
367            finally
368            {
369                IOUtil.close( fis );
370            }
371    
372            return ( versioning != null ) ? versioning : new Versioning();
373        }
374    
375        private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
376                                      ArtifactRepository repository, Exception exception )
377        {
378            DefaultRepositoryEvent event = new DefaultRepositoryEvent( EventType.METADATA_INVALID, session, trace );
379            event.setMetadata( metadata );
380            event.setException( exception );
381            event.setRepository( repository );
382    
383            repositoryEventDispatcher.dispatch( event );
384        }
385    
386        private void merge( Artifact artifact, Map<String, VersionInfo> infos, Versioning versioning,
387                            ArtifactRepository repository )
388        {
389            if ( StringUtils.isNotEmpty( versioning.getRelease() ) )
390            {
391                merge( RELEASE, infos, versioning.getLastUpdated(), versioning.getRelease(), repository );
392            }
393    
394            if ( StringUtils.isNotEmpty( versioning.getLatest() ) )
395            {
396                merge( LATEST, infos, versioning.getLastUpdated(), versioning.getLatest(), repository );
397            }
398    
399            for ( SnapshotVersion sv : versioning.getSnapshotVersions() )
400            {
401                if ( StringUtils.isNotEmpty( sv.getVersion() ) )
402                {
403                    String key = getKey( sv.getClassifier(), sv.getExtension() );
404                    merge( SNAPSHOT + key, infos, sv.getUpdated(), sv.getVersion(), repository );
405                }
406            }
407    
408            Snapshot snapshot = versioning.getSnapshot();
409            if ( snapshot != null && versioning.getSnapshotVersions().isEmpty() )
410            {
411                String version = artifact.getVersion();
412                if ( snapshot.getTimestamp() != null && snapshot.getBuildNumber() > 0 )
413                {
414                    String qualifier = snapshot.getTimestamp() + '-' + snapshot.getBuildNumber();
415                    version = version.substring( 0, version.length() - SNAPSHOT.length() ) + qualifier;
416                }
417                merge( SNAPSHOT, infos, versioning.getLastUpdated(), version, repository );
418            }
419        }
420    
421        private void merge( String key, Map<String, VersionInfo> infos, String timestamp, String version,
422                            ArtifactRepository repository )
423        {
424            VersionInfo info = infos.get( key );
425            if ( info == null )
426            {
427                info = new VersionInfo( timestamp, version, repository );
428                infos.put( key, info );
429            }
430            else if ( info.isOutdated( timestamp ) )
431            {
432                info.version = version;
433                info.repository = repository;
434                info.timestamp = timestamp;
435            }
436        }
437    
438        private void merge( Map<String, VersionInfo> infos, String srcKey, String dstKey )
439        {
440            VersionInfo srcInfo = infos.get( srcKey );
441            VersionInfo dstInfo = infos.get( dstKey );
442    
443            if ( dstInfo == null
444                || ( srcInfo != null && dstInfo.isOutdated( srcInfo.timestamp ) && srcInfo.repository != dstInfo.repository ) )
445            {
446                infos.put( dstKey, srcInfo );
447            }
448        }
449    
450        private String getKey( String classifier, String extension )
451        {
452            return StringUtils.clean( classifier ) + ':' + StringUtils.clean( extension );
453        }
454    
455        private boolean isSafelyCacheable( RepositorySystemSession session, Artifact artifact )
456        {
457            /*
458             * The workspace/reactor is in flux so we better not assume definitive information for any of its
459             * artifacts/projects.
460             */
461    
462            WorkspaceReader workspace = session.getWorkspaceReader();
463            if ( workspace == null )
464            {
465                return true;
466            }
467    
468            Artifact pomArtifact = ArtifactDescriptorUtils.toPomArtifact( artifact );
469    
470            return workspace.findArtifact( pomArtifact ) == null;
471        }
472    
473        private static class VersionInfo
474        {
475    
476            String timestamp;
477    
478            String version;
479    
480            ArtifactRepository repository;
481    
482            public VersionInfo( String timestamp, String version, ArtifactRepository repository )
483            {
484                this.timestamp = ( timestamp != null ) ? timestamp : "";
485                this.version = version;
486                this.repository = repository;
487            }
488    
489            public boolean isOutdated( String timestamp )
490            {
491                return timestamp != null && timestamp.compareTo( this.timestamp ) > 0;
492            }
493    
494        }
495    
496        private static class Key
497        {
498    
499            private final String groupId;
500    
501            private final String artifactId;
502    
503            private final String classifier;
504    
505            private final String extension;
506    
507            private final String version;
508    
509            private final String context;
510    
511            private final File localRepo;
512    
513            private final WorkspaceRepository workspace;
514    
515            private final List<RemoteRepository> repositories;
516    
517            private final int hashCode;
518    
519            public Key( RepositorySystemSession session, VersionRequest request )
520            {
521                Artifact artifact = request.getArtifact();
522                groupId = artifact.getGroupId();
523                artifactId = artifact.getArtifactId();
524                classifier = artifact.getClassifier();
525                extension = artifact.getExtension();
526                version = artifact.getVersion();
527                localRepo = session.getLocalRepository().getBasedir();
528                workspace = CacheUtils.getWorkspace( session );
529                repositories = new ArrayList<RemoteRepository>( request.getRepositories().size() );
530                boolean repoMan = false;
531                for ( RemoteRepository repository : request.getRepositories() )
532                {
533                    if ( repository.isRepositoryManager() )
534                    {
535                        repoMan = true;
536                        repositories.addAll( repository.getMirroredRepositories() );
537                    }
538                    else
539                    {
540                        repositories.add( repository );
541                    }
542                }
543                context = repoMan ? request.getRequestContext() : "";
544    
545                int hash = 17;
546                hash = hash * 31 + groupId.hashCode();
547                hash = hash * 31 + artifactId.hashCode();
548                hash = hash * 31 + classifier.hashCode();
549                hash = hash * 31 + extension.hashCode();
550                hash = hash * 31 + version.hashCode();
551                hash = hash * 31 + localRepo.hashCode();
552                hash = hash * 31 + CacheUtils.repositoriesHashCode( repositories );
553                hashCode = hash;
554            }
555    
556            @Override
557            public boolean equals( Object obj )
558            {
559                if ( obj == this )
560                {
561                    return true;
562                }
563                else if ( obj == null || !getClass().equals( obj.getClass() ) )
564                {
565                    return false;
566                }
567    
568                Key that = (Key) obj;
569                return artifactId.equals( that.artifactId ) && groupId.equals( that.groupId )
570                    && classifier.equals( that.classifier ) && extension.equals( that.extension )
571                    && version.equals( that.version ) && context.equals( that.context )
572                    && localRepo.equals( that.localRepo ) && CacheUtils.eq( workspace, that.workspace )
573                    && CacheUtils.repositoriesEquals( repositories, that.repositories );
574            }
575    
576            @Override
577            public int hashCode()
578            {
579                return hashCode;
580            }
581    
582        }
583    
584        private static class Record
585        {
586            final String version;
587    
588            final String repoId;
589    
590            final Class<?> repoClass;
591    
592            public Record( String version, ArtifactRepository repository )
593            {
594                this.version = version;
595                if ( repository != null )
596                {
597                    repoId = repository.getId();
598                    repoClass = repository.getClass();
599                }
600                else
601                {
602                    repoId = null;
603                    repoClass = null;
604                }
605            }
606        }
607    
608    }