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