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> metadataReqs = new ArrayList<MetadataRequest>( request.getRepositories().size() );
237
238            metadataReqs.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                metadataReqs.add( metadataRequest );
248            }
249
250            List<MetadataResult> metadataResults = metadataResolver.resolveMetadata( session, metadataReqs );
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 v = readVersions( session, trace, metadataResult.getMetadata(), repository, result );
265                merge( artifact, infos, v, 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                        RemoteRepository r = (RemoteRepository) result.getRepository();
286                        subRequest.setRepositories( Collections.singletonList( r ) );
287                    }
288                    else
289                    {
290                        subRequest.setRepositories( request.getRepositories() );
291                    }
292                    VersionResult subResult = resolveVersion( session, subRequest );
293                    result.setVersion( subResult.getVersion() );
294                    result.setRepository( subResult.getRepository() );
295                    for ( Exception exception : subResult.getExceptions() )
296                    {
297                        result.addException( exception );
298                    }
299                }
300            }
301            else
302            {
303                String key = SNAPSHOT + getKey( artifact.getClassifier(), artifact.getExtension() );
304                merge( infos, SNAPSHOT, key );
305                if ( !resolve( result, infos, key ) )
306                {
307                    result.setVersion( version );
308                }
309            }
310
311            if ( StringUtils.isEmpty( result.getVersion() ) )
312            {
313                throw new VersionResolutionException( result );
314            }
315        }
316
317        if ( cacheKey != null && metadata != null && isSafelyCacheable( session, artifact ) )
318        {
319            cache.put( session, cacheKey, new Record( result.getVersion(), result.getRepository() ) );
320        }
321
322        return result;
323    }
324
325    private boolean resolve( VersionResult result, Map<String, VersionInfo> infos, String key )
326    {
327        VersionInfo info = infos.get( key );
328        if ( info != null )
329        {
330            result.setVersion( info.version );
331            result.setRepository( info.repository );
332        }
333        return info != null;
334    }
335
336    private Versioning readVersions( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
337                                     ArtifactRepository repository, VersionResult result )
338    {
339        Versioning versioning = null;
340
341        FileInputStream fis = null;
342        try
343        {
344            if ( metadata != null )
345            {
346                SyncContext syncContext = syncContextFactory.newInstance( session, true );
347
348                try
349                {
350                    syncContext.acquire( null, Collections.singleton( metadata ) );
351
352                    if ( metadata.getFile() != null && metadata.getFile().exists() )
353                    {
354                        fis = new FileInputStream( metadata.getFile() );
355                        org.apache.maven.artifact.repository.metadata.Metadata m =
356                            new MetadataXpp3Reader().read( fis, false );
357                        versioning = m.getVersioning();
358
359                        /*
360                         * NOTE: Users occasionally misuse the id "local" for remote repos which screws up the metadata
361                         * of the local repository. This is especially troublesome during snapshot resolution so we try
362                         * to handle that gracefully.
363                         */
364                        if ( versioning != null && repository instanceof LocalRepository )
365                        {
366                            if ( versioning.getSnapshot() != null && versioning.getSnapshot().getBuildNumber() > 0 )
367                            {
368                                Versioning repaired = new Versioning();
369                                repaired.setLastUpdated( versioning.getLastUpdated() );
370                                Snapshot snapshot = new Snapshot();
371                                snapshot.setLocalCopy( true );
372                                repaired.setSnapshot( snapshot );
373                                versioning = repaired;
374
375                                throw new IOException( "Snapshot information corrupted with remote repository data"
376                                    + ", please verify that no remote repository uses the id '" + repository.getId()
377                                    + "'" );
378                            }
379                        }
380                    }
381                }
382                finally
383                {
384                    syncContext.close();
385                }
386            }
387        }
388        catch ( Exception e )
389        {
390            invalidMetadata( session, trace, metadata, repository, e );
391            result.addException( e );
392        }
393        finally
394        {
395            IOUtil.close( fis );
396        }
397
398        return ( versioning != null ) ? versioning : new Versioning();
399    }
400
401    private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
402                                  ArtifactRepository repository, Exception exception )
403    {
404        RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_INVALID );
405        event.setTrace( trace );
406        event.setMetadata( metadata );
407        event.setException( exception );
408        event.setRepository( repository );
409
410        repositoryEventDispatcher.dispatch( event.build() );
411    }
412
413    private void merge( Artifact artifact, Map<String, VersionInfo> infos, Versioning versioning,
414                        ArtifactRepository repository )
415    {
416        if ( StringUtils.isNotEmpty( versioning.getRelease() ) )
417        {
418            merge( RELEASE, infos, versioning.getLastUpdated(), versioning.getRelease(), repository );
419        }
420
421        if ( StringUtils.isNotEmpty( versioning.getLatest() ) )
422        {
423            merge( LATEST, infos, versioning.getLastUpdated(), versioning.getLatest(), repository );
424        }
425
426        for ( SnapshotVersion sv : versioning.getSnapshotVersions() )
427        {
428            if ( StringUtils.isNotEmpty( sv.getVersion() ) )
429            {
430                String key = getKey( sv.getClassifier(), sv.getExtension() );
431                merge( SNAPSHOT + key, infos, sv.getUpdated(), sv.getVersion(), repository );
432            }
433        }
434
435        Snapshot snapshot = versioning.getSnapshot();
436        if ( snapshot != null && versioning.getSnapshotVersions().isEmpty() )
437        {
438            String version = artifact.getVersion();
439            if ( snapshot.getTimestamp() != null && snapshot.getBuildNumber() > 0 )
440            {
441                String qualifier = snapshot.getTimestamp() + '-' + snapshot.getBuildNumber();
442                version = version.substring( 0, version.length() - SNAPSHOT.length() ) + qualifier;
443            }
444            merge( SNAPSHOT, infos, versioning.getLastUpdated(), version, repository );
445        }
446    }
447
448    private void merge( String key, Map<String, VersionInfo> infos, String timestamp, String version,
449                        ArtifactRepository repository )
450    {
451        VersionInfo info = infos.get( key );
452        if ( info == null )
453        {
454            info = new VersionInfo( timestamp, version, repository );
455            infos.put( key, info );
456        }
457        else if ( info.isOutdated( timestamp ) )
458        {
459            info.version = version;
460            info.repository = repository;
461            info.timestamp = timestamp;
462        }
463    }
464
465    private void merge( Map<String, VersionInfo> infos, String srcKey, String dstKey )
466    {
467        VersionInfo srcInfo = infos.get( srcKey );
468        VersionInfo dstInfo = infos.get( dstKey );
469
470        if ( dstInfo == null
471            || ( srcInfo != null && dstInfo.isOutdated( srcInfo.timestamp )
472                 && srcInfo.repository != dstInfo.repository ) )
473        {
474            infos.put( dstKey, srcInfo );
475        }
476    }
477
478    private String getKey( String classifier, String extension )
479    {
480        return StringUtils.clean( classifier ) + ':' + StringUtils.clean( extension );
481    }
482
483    private boolean isSafelyCacheable( RepositorySystemSession session, Artifact artifact )
484    {
485        /*
486         * The workspace/reactor is in flux so we better not assume definitive information for any of its
487         * artifacts/projects.
488         */
489
490        WorkspaceReader workspace = session.getWorkspaceReader();
491        if ( workspace == null )
492        {
493            return true;
494        }
495
496        Artifact pomArtifact = ArtifactDescriptorUtils.toPomArtifact( artifact );
497
498        return workspace.findArtifact( pomArtifact ) == null;
499    }
500
501    private static class VersionInfo
502    {
503
504        String timestamp;
505
506        String version;
507
508        ArtifactRepository repository;
509
510        public VersionInfo( String timestamp, String version, ArtifactRepository repository )
511        {
512            this.timestamp = ( timestamp != null ) ? timestamp : "";
513            this.version = version;
514            this.repository = repository;
515        }
516
517        public boolean isOutdated( String timestamp )
518        {
519            return timestamp != null && timestamp.compareTo( this.timestamp ) > 0;
520        }
521
522    }
523
524    private static class Key
525    {
526
527        private final String groupId;
528
529        private final String artifactId;
530
531        private final String classifier;
532
533        private final String extension;
534
535        private final String version;
536
537        private final String context;
538
539        private final File localRepo;
540
541        private final WorkspaceRepository workspace;
542
543        private final List<RemoteRepository> repositories;
544
545        private final int hashCode;
546
547        public Key( RepositorySystemSession session, VersionRequest request )
548        {
549            Artifact artifact = request.getArtifact();
550            groupId = artifact.getGroupId();
551            artifactId = artifact.getArtifactId();
552            classifier = artifact.getClassifier();
553            extension = artifact.getExtension();
554            version = artifact.getVersion();
555            localRepo = session.getLocalRepository().getBasedir();
556            workspace = CacheUtils.getWorkspace( session );
557            repositories = new ArrayList<RemoteRepository>( request.getRepositories().size() );
558            boolean repoMan = false;
559            for ( RemoteRepository repository : request.getRepositories() )
560            {
561                if ( repository.isRepositoryManager() )
562                {
563                    repoMan = true;
564                    repositories.addAll( repository.getMirroredRepositories() );
565                }
566                else
567                {
568                    repositories.add( repository );
569                }
570            }
571            context = repoMan ? request.getRequestContext() : "";
572
573            int hash = 17;
574            hash = hash * 31 + groupId.hashCode();
575            hash = hash * 31 + artifactId.hashCode();
576            hash = hash * 31 + classifier.hashCode();
577            hash = hash * 31 + extension.hashCode();
578            hash = hash * 31 + version.hashCode();
579            hash = hash * 31 + localRepo.hashCode();
580            hash = hash * 31 + CacheUtils.repositoriesHashCode( repositories );
581            hashCode = hash;
582        }
583
584        @Override
585        public boolean equals( Object obj )
586        {
587            if ( obj == this )
588            {
589                return true;
590            }
591            else if ( obj == null || !getClass().equals( obj.getClass() ) )
592            {
593                return false;
594            }
595
596            Key that = (Key) obj;
597            return artifactId.equals( that.artifactId ) && groupId.equals( that.groupId )
598                && classifier.equals( that.classifier ) && extension.equals( that.extension )
599                && version.equals( that.version ) && context.equals( that.context )
600                && localRepo.equals( that.localRepo ) && CacheUtils.eq( workspace, that.workspace )
601                && CacheUtils.repositoriesEquals( repositories, that.repositories );
602        }
603
604        @Override
605        public int hashCode()
606        {
607            return hashCode;
608        }
609
610    }
611
612    private static class Record
613    {
614        final String version;
615
616        final String repoId;
617
618        final Class<?> repoClass;
619
620        public Record( String version, ArtifactRepository repository )
621        {
622            this.version = version;
623            if ( repository != null )
624            {
625                repoId = repository.getId();
626                repoClass = repository.getClass();
627            }
628            else
629            {
630                repoId = null;
631                repoClass = null;
632            }
633        }
634    }
635
636}