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