View Javadoc
1   package org.apache.maven.repository.internal;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.artifact.repository.metadata.Snapshot;
23  import org.apache.maven.artifact.repository.metadata.SnapshotVersion;
24  import org.apache.maven.artifact.repository.metadata.Versioning;
25  import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
26  import org.codehaus.plexus.util.StringUtils;
27  import org.eclipse.aether.RepositoryCache;
28  import org.eclipse.aether.RepositoryEvent;
29  import org.eclipse.aether.RepositoryEvent.EventType;
30  import org.eclipse.aether.RepositorySystemSession;
31  import org.eclipse.aether.RequestTrace;
32  import org.eclipse.aether.SyncContext;
33  import org.eclipse.aether.artifact.Artifact;
34  import org.eclipse.aether.impl.MetadataResolver;
35  import org.eclipse.aether.impl.RepositoryEventDispatcher;
36  import org.eclipse.aether.impl.SyncContextFactory;
37  import org.eclipse.aether.impl.VersionResolver;
38  import org.eclipse.aether.internal.impl.CacheUtils;
39  import org.eclipse.aether.metadata.DefaultMetadata;
40  import org.eclipse.aether.metadata.Metadata;
41  import org.eclipse.aether.repository.ArtifactRepository;
42  import org.eclipse.aether.repository.LocalRepository;
43  import org.eclipse.aether.repository.RemoteRepository;
44  import org.eclipse.aether.repository.WorkspaceReader;
45  import org.eclipse.aether.repository.WorkspaceRepository;
46  import org.eclipse.aether.resolution.MetadataRequest;
47  import org.eclipse.aether.resolution.MetadataResult;
48  import org.eclipse.aether.resolution.VersionRequest;
49  import org.eclipse.aether.resolution.VersionResolutionException;
50  import org.eclipse.aether.resolution.VersionResult;
51  import org.eclipse.aether.spi.locator.Service;
52  import org.eclipse.aether.spi.locator.ServiceLocator;
53  import org.eclipse.aether.util.ConfigUtils;
54  
55  import javax.inject.Inject;
56  import javax.inject.Named;
57  import javax.inject.Singleton;
58  
59  import java.io.File;
60  import java.io.FileInputStream;
61  import java.io.IOException;
62  import java.io.InputStream;
63  import java.util.ArrayList;
64  import java.util.Collections;
65  import java.util.HashMap;
66  import java.util.List;
67  import java.util.Map;
68  import java.util.Objects;
69  
70  /**
71   * @author Benjamin Bentmann
72   */
73  @Named
74  @Singleton
75  public class DefaultVersionResolver
76      implements VersionResolver, Service
77  {
78  
79      private static final String MAVEN_METADATA_XML = "maven-metadata.xml";
80  
81      private static final String RELEASE = "RELEASE";
82  
83      private static final String LATEST = "LATEST";
84  
85      private static final String SNAPSHOT = "SNAPSHOT";
86  
87      private MetadataResolver metadataResolver;
88  
89      private SyncContextFactory syncContextFactory;
90  
91      private RepositoryEventDispatcher repositoryEventDispatcher;
92  
93      public DefaultVersionResolver()
94      {
95          // enable no-arg constructor
96      }
97  
98      @Inject
99      DefaultVersionResolver( MetadataResolver metadataResolver, SyncContextFactory syncContextFactory,
100                             RepositoryEventDispatcher repositoryEventDispatcher )
101     {
102         setMetadataResolver( metadataResolver );
103         setSyncContextFactory( syncContextFactory );
104         setRepositoryEventDispatcher( repositoryEventDispatcher );
105     }
106 
107     public void initService( ServiceLocator locator )
108     {
109         setMetadataResolver( locator.getService( MetadataResolver.class ) );
110         setSyncContextFactory( locator.getService( SyncContextFactory.class ) );
111         setRepositoryEventDispatcher( locator.getService( RepositoryEventDispatcher.class ) );
112     }
113 
114     public DefaultVersionResolver setMetadataResolver( MetadataResolver metadataResolver )
115     {
116         this.metadataResolver = Objects.requireNonNull( metadataResolver, "metadataResolver cannot be null" );
117         return this;
118     }
119 
120     public DefaultVersionResolver setSyncContextFactory( SyncContextFactory syncContextFactory )
121     {
122         this.syncContextFactory = Objects.requireNonNull( syncContextFactory, "syncContextFactory cannot be null" );
123         return this;
124     }
125 
126     public DefaultVersionResolver setRepositoryEventDispatcher( RepositoryEventDispatcher repositoryEventDispatcher )
127     {
128         this.repositoryEventDispatcher = Objects.requireNonNull( repositoryEventDispatcher,
129             "repositoryEventDispatcher cannot be null" );
130         return this;
131     }
132 
133     @SuppressWarnings( "checkstyle:methodlength" )
134     public VersionResult resolveVersion( RepositorySystemSession session, VersionRequest request )
135         throws VersionResolutionException
136     {
137         RequestTrace trace = RequestTrace.newChild( request.getTrace(), request );
138 
139         Artifact artifact = request.getArtifact();
140 
141         String version = artifact.getVersion();
142 
143         VersionResult result = new VersionResult( request );
144 
145         Key cacheKey = null;
146         RepositoryCache cache = session.getCache();
147         if ( cache != null && !ConfigUtils.getBoolean( session, false, "aether.versionResolver.noCache" ) )
148         {
149             cacheKey = new Key( session, request );
150 
151             Object obj = cache.get( session, cacheKey );
152             if ( obj instanceof Record )
153             {
154                 Record record = (Record) obj;
155                 result.setVersion( record.version );
156                 result.setRepository(
157                     CacheUtils.getRepository( session, request.getRepositories(), record.repoClass, record.repoId ) );
158                 return result;
159             }
160         }
161 
162         Metadata metadata;
163 
164         if ( RELEASE.equals( version ) )
165         {
166             metadata = new DefaultMetadata( artifact.getGroupId(), artifact.getArtifactId(), MAVEN_METADATA_XML,
167                                             Metadata.Nature.RELEASE );
168         }
169         else if ( LATEST.equals( version ) )
170         {
171             metadata = new DefaultMetadata( artifact.getGroupId(), artifact.getArtifactId(), MAVEN_METADATA_XML,
172                                             Metadata.Nature.RELEASE_OR_SNAPSHOT );
173         }
174         else if ( version.endsWith( SNAPSHOT ) )
175         {
176             WorkspaceReader workspace = session.getWorkspaceReader();
177             if ( workspace != null && workspace.findVersions( artifact ).contains( version ) )
178             {
179                 metadata = null;
180                 result.setRepository( workspace.getRepository() );
181             }
182             else
183             {
184                 metadata =
185                     new DefaultMetadata( artifact.getGroupId(), artifact.getArtifactId(), version, MAVEN_METADATA_XML,
186                                          Metadata.Nature.SNAPSHOT );
187             }
188         }
189         else
190         {
191             metadata = null;
192         }
193 
194         if ( metadata == null )
195         {
196             result.setVersion( version );
197         }
198         else
199         {
200             List<MetadataRequest> metadataReqs = new ArrayList<>( request.getRepositories().size() );
201 
202             metadataReqs.add( new MetadataRequest( metadata, null, request.getRequestContext() ) );
203 
204             for ( RemoteRepository repository : request.getRepositories() )
205             {
206                 MetadataRequest metadataRequest =
207                     new MetadataRequest( metadata, repository, request.getRequestContext() );
208                 metadataRequest.setDeleteLocalCopyIfMissing( true );
209                 metadataRequest.setFavorLocalRepository( true );
210                 metadataRequest.setTrace( trace );
211                 metadataReqs.add( metadataRequest );
212             }
213 
214             List<MetadataResult> metadataResults = metadataResolver.resolveMetadata( session, metadataReqs );
215 
216             Map<String, VersionInfo> infos = new HashMap<>();
217 
218             for ( MetadataResult metadataResult : metadataResults )
219             {
220                 result.addException( metadataResult.getException() );
221 
222                 ArtifactRepository repository = metadataResult.getRequest().getRepository();
223                 if ( repository == null )
224                 {
225                     repository = session.getLocalRepository();
226                 }
227 
228                 Versioning v = readVersions( session, trace, metadataResult.getMetadata(), repository, result );
229                 merge( artifact, infos, v, repository );
230             }
231 
232             if ( RELEASE.equals( version ) )
233             {
234                 resolve( result, infos, RELEASE );
235             }
236             else if ( LATEST.equals( version ) )
237             {
238                 if ( !resolve( result, infos, LATEST ) )
239                 {
240                     resolve( result, infos, RELEASE );
241                 }
242 
243                 if ( result.getVersion() != null && result.getVersion().endsWith( SNAPSHOT ) )
244                 {
245                     VersionRequest subRequest = new VersionRequest();
246                     subRequest.setArtifact( artifact.setVersion( result.getVersion() ) );
247                     if ( result.getRepository() instanceof RemoteRepository )
248                     {
249                         RemoteRepository r = (RemoteRepository) result.getRepository();
250                         subRequest.setRepositories( Collections.singletonList( r ) );
251                     }
252                     else
253                     {
254                         subRequest.setRepositories( request.getRepositories() );
255                     }
256                     VersionResult subResult = resolveVersion( session, subRequest );
257                     result.setVersion( subResult.getVersion() );
258                     result.setRepository( subResult.getRepository() );
259                     for ( Exception exception : subResult.getExceptions() )
260                     {
261                         result.addException( exception );
262                     }
263                 }
264             }
265             else
266             {
267                 String key = SNAPSHOT + getKey( artifact.getClassifier(), artifact.getExtension() );
268                 merge( infos, SNAPSHOT, key );
269                 if ( !resolve( result, infos, key ) )
270                 {
271                     result.setVersion( version );
272                 }
273             }
274 
275             if ( StringUtils.isEmpty( result.getVersion() ) )
276             {
277                 throw new VersionResolutionException( result );
278             }
279         }
280 
281         if ( cacheKey != null && metadata != null && isSafelyCacheable( session, artifact ) )
282         {
283             cache.put( session, cacheKey, new Record( result.getVersion(), result.getRepository() ) );
284         }
285 
286         return result;
287     }
288 
289     private boolean resolve( VersionResult result, Map<String, VersionInfo> infos, String key )
290     {
291         VersionInfo info = infos.get( key );
292         if ( info != null )
293         {
294             result.setVersion( info.version );
295             result.setRepository( info.repository );
296         }
297         return info != null;
298     }
299 
300     private Versioning readVersions( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
301                                      ArtifactRepository repository, VersionResult result )
302     {
303         Versioning versioning = null;
304         try
305         {
306             if ( metadata != null )
307             {
308                 try ( SyncContext syncContext = syncContextFactory.newInstance( session, true ) )
309                 {
310                     syncContext.acquire( null, Collections.singleton( metadata ) );
311 
312                     if ( metadata.getFile() != null && metadata.getFile().exists() )
313                     {
314                         try ( final InputStream in = new FileInputStream( metadata.getFile() ) )
315                         {
316                             versioning = new MetadataXpp3Reader().read( in, false ).getVersioning();
317 
318                             /*
319                             NOTE: Users occasionally misuse the id "local" for remote repos which screws up the metadata
320                             of the local repository. This is especially troublesome during snapshot resolution so we try
321                             to handle that gracefully.
322                              */
323                             if ( versioning != null && repository instanceof LocalRepository
324                                      && versioning.getSnapshot() != null
325                                      && versioning.getSnapshot().getBuildNumber() > 0 )
326                             {
327                                 final Versioning repaired = new Versioning();
328                                 repaired.setLastUpdated( versioning.getLastUpdated() );
329                                 repaired.setSnapshot( new Snapshot() );
330                                 repaired.getSnapshot().setLocalCopy( true );
331                                 versioning = repaired;
332                                 throw new IOException( "Snapshot information corrupted with remote repository data"
333                                                            + ", please verify that no remote repository uses the id '"
334                                                            + repository.getId() + "'" );
335 
336                             }
337                         }
338                     }
339                 }
340             }
341         }
342         catch ( Exception e )
343         {
344             invalidMetadata( session, trace, metadata, repository, e );
345             result.addException( e );
346         }
347 
348         return ( versioning != null ) ? versioning : new Versioning();
349     }
350 
351     private void invalidMetadata( RepositorySystemSession session, RequestTrace trace, Metadata metadata,
352                                   ArtifactRepository repository, Exception exception )
353     {
354         RepositoryEvent.Builder event = new RepositoryEvent.Builder( session, EventType.METADATA_INVALID );
355         event.setTrace( trace );
356         event.setMetadata( metadata );
357         event.setException( exception );
358         event.setRepository( repository );
359 
360         repositoryEventDispatcher.dispatch( event.build() );
361     }
362 
363     private void merge( Artifact artifact, Map<String, VersionInfo> infos, Versioning versioning,
364                         ArtifactRepository repository )
365     {
366         if ( StringUtils.isNotEmpty( versioning.getRelease() ) )
367         {
368             merge( RELEASE, infos, versioning.getLastUpdated(), versioning.getRelease(), repository );
369         }
370 
371         if ( StringUtils.isNotEmpty( versioning.getLatest() ) )
372         {
373             merge( LATEST, infos, versioning.getLastUpdated(), versioning.getLatest(), repository );
374         }
375 
376         for ( SnapshotVersion sv : versioning.getSnapshotVersions() )
377         {
378             if ( StringUtils.isNotEmpty( sv.getVersion() ) )
379             {
380                 String key = getKey( sv.getClassifier(), sv.getExtension() );
381                 merge( SNAPSHOT + key, infos, sv.getUpdated(), sv.getVersion(), repository );
382             }
383         }
384 
385         Snapshot snapshot = versioning.getSnapshot();
386         if ( snapshot != null && versioning.getSnapshotVersions().isEmpty() )
387         {
388             String version = artifact.getVersion();
389             if ( snapshot.getTimestamp() != null && snapshot.getBuildNumber() > 0 )
390             {
391                 String qualifier = snapshot.getTimestamp() + '-' + snapshot.getBuildNumber();
392                 version = version.substring( 0, version.length() - SNAPSHOT.length() ) + qualifier;
393             }
394             merge( SNAPSHOT, infos, versioning.getLastUpdated(), version, repository );
395         }
396     }
397 
398     private void merge( String key, Map<String, VersionInfo> infos, String timestamp, String version,
399                         ArtifactRepository repository )
400     {
401         VersionInfo info = infos.get( key );
402         if ( info == null )
403         {
404             info = new VersionInfo( timestamp, version, repository );
405             infos.put( key, info );
406         }
407         else if ( info.isOutdated( timestamp ) )
408         {
409             info.version = version;
410             info.repository = repository;
411             info.timestamp = timestamp;
412         }
413     }
414 
415     private void merge( Map<String, VersionInfo> infos, String srcKey, String dstKey )
416     {
417         VersionInfo srcInfo = infos.get( srcKey );
418         VersionInfo dstInfo = infos.get( dstKey );
419 
420         if ( dstInfo == null || ( srcInfo != null && dstInfo.isOutdated( srcInfo.timestamp )
421             && srcInfo.repository != dstInfo.repository ) )
422         {
423             infos.put( dstKey, srcInfo );
424         }
425     }
426 
427     private String getKey( String classifier, String extension )
428     {
429         return StringUtils.clean( classifier ) + ':' + StringUtils.clean( extension );
430     }
431 
432     private boolean isSafelyCacheable( RepositorySystemSession session, Artifact artifact )
433     {
434         /*
435          * The workspace/reactor is in flux so we better not assume definitive information for any of its
436          * artifacts/projects.
437          */
438 
439         WorkspaceReader workspace = session.getWorkspaceReader();
440         if ( workspace == null )
441         {
442             return true;
443         }
444 
445         Artifact pomArtifact = ArtifactDescriptorUtils.toPomArtifact( artifact );
446 
447         return workspace.findArtifact( pomArtifact ) == null;
448     }
449 
450     private static class VersionInfo
451     {
452 
453         String timestamp;
454 
455         String version;
456 
457         ArtifactRepository repository;
458 
459         VersionInfo( String timestamp, String version, ArtifactRepository repository )
460         {
461             this.timestamp = ( timestamp != null ) ? timestamp : "";
462             this.version = version;
463             this.repository = repository;
464         }
465 
466         boolean isOutdated( String timestamp )
467         {
468             return timestamp != null && timestamp.compareTo( this.timestamp ) > 0;
469         }
470 
471     }
472 
473     private static class Key
474     {
475 
476         private final String groupId;
477 
478         private final String artifactId;
479 
480         private final String classifier;
481 
482         private final String extension;
483 
484         private final String version;
485 
486         private final String context;
487 
488         private final File localRepo;
489 
490         private final WorkspaceRepository workspace;
491 
492         private final List<RemoteRepository> repositories;
493 
494         private final int hashCode;
495 
496         Key( RepositorySystemSession session, VersionRequest request )
497         {
498             Artifact artifact = request.getArtifact();
499             groupId = artifact.getGroupId();
500             artifactId = artifact.getArtifactId();
501             classifier = artifact.getClassifier();
502             extension = artifact.getExtension();
503             version = artifact.getVersion();
504             localRepo = session.getLocalRepository().getBasedir();
505             workspace = CacheUtils.getWorkspace( session );
506             repositories = new ArrayList<>( request.getRepositories().size() );
507             boolean repoMan = false;
508             for ( RemoteRepository repository : request.getRepositories() )
509             {
510                 if ( repository.isRepositoryManager() )
511                 {
512                     repoMan = true;
513                     repositories.addAll( repository.getMirroredRepositories() );
514                 }
515                 else
516                 {
517                     repositories.add( repository );
518                 }
519             }
520             context = repoMan ? request.getRequestContext() : "";
521 
522             int hash = 17;
523             hash = hash * 31 + groupId.hashCode();
524             hash = hash * 31 + artifactId.hashCode();
525             hash = hash * 31 + classifier.hashCode();
526             hash = hash * 31 + extension.hashCode();
527             hash = hash * 31 + version.hashCode();
528             hash = hash * 31 + localRepo.hashCode();
529             hash = hash * 31 + CacheUtils.repositoriesHashCode( repositories );
530             hashCode = hash;
531         }
532 
533         @Override
534         public boolean equals( Object obj )
535         {
536             if ( obj == this )
537             {
538                 return true;
539             }
540             else if ( obj == null || !getClass().equals( obj.getClass() ) )
541             {
542                 return false;
543             }
544 
545             Key that = (Key) obj;
546             return artifactId.equals( that.artifactId ) && groupId.equals( that.groupId ) && classifier.equals(
547                 that.classifier ) && extension.equals( that.extension ) && version.equals( that.version )
548                 && context.equals( that.context ) && localRepo.equals( that.localRepo )
549                 && CacheUtils.eq( workspace, that.workspace )
550                 && CacheUtils.repositoriesEquals( repositories, that.repositories );
551         }
552 
553         @Override
554         public int hashCode()
555         {
556             return hashCode;
557         }
558 
559     }
560 
561     private static class Record
562     {
563         final String version;
564 
565         final String repoId;
566 
567         final Class<?> repoClass;
568 
569         Record( String version, ArtifactRepository repository )
570         {
571             this.version = version;
572             if ( repository != null )
573             {
574                 repoId = repository.getId();
575                 repoClass = repository.getClass();
576             }
577             else
578             {
579                 repoId = null;
580                 repoClass = null;
581             }
582         }
583     }
584 
585 }