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