001package 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
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030
031import javax.inject.Inject;
032import javax.inject.Named;
033
034import org.apache.maven.artifact.repository.metadata.Snapshot;
035import org.apache.maven.artifact.repository.metadata.SnapshotVersion;
036import org.apache.maven.artifact.repository.metadata.Versioning;
037import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
038import org.codehaus.plexus.component.annotations.Component;
039import org.codehaus.plexus.component.annotations.Requirement;
040import org.codehaus.plexus.util.IOUtil;
041import org.codehaus.plexus.util.StringUtils;
042import org.eclipse.aether.RepositoryCache;
043import org.eclipse.aether.RepositoryEvent.EventType;
044import org.eclipse.aether.RepositoryEvent;
045import org.eclipse.aether.RepositorySystemSession;
046import org.eclipse.aether.RequestTrace;
047import org.eclipse.aether.SyncContext;
048import org.eclipse.aether.artifact.Artifact;
049import org.eclipse.aether.impl.MetadataResolver;
050import org.eclipse.aether.impl.RepositoryEventDispatcher;
051import org.eclipse.aether.impl.SyncContextFactory;
052import org.eclipse.aether.impl.VersionResolver;
053import org.eclipse.aether.internal.impl.CacheUtils;
054import org.eclipse.aether.metadata.DefaultMetadata;
055import org.eclipse.aether.metadata.Metadata;
056import org.eclipse.aether.repository.ArtifactRepository;
057import org.eclipse.aether.repository.LocalRepository;
058import org.eclipse.aether.repository.RemoteRepository;
059import org.eclipse.aether.repository.WorkspaceReader;
060import org.eclipse.aether.repository.WorkspaceRepository;
061import org.eclipse.aether.resolution.MetadataRequest;
062import org.eclipse.aether.resolution.MetadataResult;
063import org.eclipse.aether.resolution.VersionRequest;
064import org.eclipse.aether.resolution.VersionResolutionException;
065import org.eclipse.aether.resolution.VersionResult;
066import org.eclipse.aether.spi.locator.Service;
067import org.eclipse.aether.spi.locator.ServiceLocator;
068import org.eclipse.aether.spi.log.Logger;
069import org.eclipse.aether.spi.log.LoggerFactory;
070import org.eclipse.aether.spi.log.NullLoggerFactory;
071import org.eclipse.aether.util.ConfigUtils;
072
073/**
074 * @author Benjamin Bentmann
075 */
076@Named
077@Component( role = VersionResolver.class )
078public 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}