View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.repository.internal;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.FileInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.Objects;
35  
36  import org.apache.maven.artifact.ArtifactUtils;
37  import org.apache.maven.artifact.repository.metadata.Versioning;
38  import org.apache.maven.repository.internal.metadata.ValidatingMetadataXpp3Reader;
39  import org.eclipse.aether.RepositoryEvent;
40  import org.eclipse.aether.RepositoryEvent.EventType;
41  import org.eclipse.aether.RepositorySystemSession;
42  import org.eclipse.aether.RequestTrace;
43  import org.eclipse.aether.SyncContext;
44  import org.eclipse.aether.impl.MetadataResolver;
45  import org.eclipse.aether.impl.RepositoryEventDispatcher;
46  import org.eclipse.aether.impl.VersionRangeResolver;
47  import org.eclipse.aether.metadata.DefaultMetadata;
48  import org.eclipse.aether.metadata.Metadata;
49  import org.eclipse.aether.repository.ArtifactRepository;
50  import org.eclipse.aether.repository.RemoteRepository;
51  import org.eclipse.aether.repository.WorkspaceReader;
52  import org.eclipse.aether.resolution.MetadataRequest;
53  import org.eclipse.aether.resolution.MetadataResult;
54  import org.eclipse.aether.resolution.VersionRangeRequest;
55  import org.eclipse.aether.resolution.VersionRangeResolutionException;
56  import org.eclipse.aether.resolution.VersionRangeResult;
57  import org.eclipse.aether.spi.synccontext.SyncContextFactory;
58  import org.eclipse.aether.util.ConfigUtils;
59  import org.eclipse.aether.util.version.GenericVersionScheme;
60  import org.eclipse.aether.version.InvalidVersionSpecificationException;
61  import org.eclipse.aether.version.Version;
62  import org.eclipse.aether.version.VersionConstraint;
63  import org.eclipse.aether.version.VersionRange;
64  import org.eclipse.aether.version.VersionScheme;
65  
66  /**
67   * @author Benjamin Bentmann
68   */
69  @Named
70  @Singleton
71  public class DefaultVersionRangeResolver implements VersionRangeResolver {
72      /**
73       * Configuration property for version range resolution used metadata {@link Metadata.Nature}.
74       * It may contain string names of {@link Metadata.Nature} enum values, or string value {@code "auto"}
75       * to decide based on range: if any of the boundary versions is snapshot, {@link Metadata.Nature#RELEASE_OR_SNAPSHOT}
76       * will be used, otherwise {@link Metadata.Nature#RELEASE}.
77       * Default (when unset) is existing Maven 3 behaviour, using {@link Metadata.Nature#RELEASE_OR_SNAPSHOT}.
78       *
79       * @since 3.9.11
80       */
81      public static final String MAVEN_VERSION_RANGE_RESOLUTION_NATURE = "maven.versionRangeResolutionNature";
82  
83      private static final String MAVEN_METADATA_XML = "maven-metadata.xml";
84  
85      private MetadataResolver metadataResolver;
86  
87      private SyncContextFactory syncContextFactory;
88  
89      private RepositoryEventDispatcher repositoryEventDispatcher;
90  
91      @Deprecated
92      public DefaultVersionRangeResolver() {
93          // enable default constructor
94      }
95  
96      @Inject
97      public DefaultVersionRangeResolver(
98              MetadataResolver metadataResolver,
99              SyncContextFactory syncContextFactory,
100             RepositoryEventDispatcher repositoryEventDispatcher) {
101         setMetadataResolver(metadataResolver);
102         setSyncContextFactory(syncContextFactory);
103         setRepositoryEventDispatcher(repositoryEventDispatcher);
104     }
105 
106     public DefaultVersionRangeResolver setMetadataResolver(MetadataResolver metadataResolver) {
107         this.metadataResolver = Objects.requireNonNull(metadataResolver, "metadataResolver cannot be null");
108         return this;
109     }
110 
111     public DefaultVersionRangeResolver setSyncContextFactory(SyncContextFactory syncContextFactory) {
112         this.syncContextFactory = Objects.requireNonNull(syncContextFactory, "syncContextFactory cannot be null");
113         return this;
114     }
115 
116     public DefaultVersionRangeResolver setRepositoryEventDispatcher(
117             RepositoryEventDispatcher repositoryEventDispatcher) {
118         this.repositoryEventDispatcher =
119                 Objects.requireNonNull(repositoryEventDispatcher, "repositoryEventDispatcher cannot be null");
120         return this;
121     }
122 
123     @Override
124     public VersionRangeResult resolveVersionRange(RepositorySystemSession session, VersionRangeRequest request)
125             throws VersionRangeResolutionException {
126         VersionRangeResult result = new VersionRangeResult(request);
127 
128         VersionScheme versionScheme = new GenericVersionScheme();
129 
130         VersionConstraint versionConstraint;
131         try {
132             versionConstraint =
133                     versionScheme.parseVersionConstraint(request.getArtifact().getVersion());
134         } catch (InvalidVersionSpecificationException e) {
135             result.addException(e);
136             throw new VersionRangeResolutionException(result);
137         }
138 
139         result.setVersionConstraint(versionConstraint);
140 
141         if (versionConstraint.getRange() == null) {
142             result.addVersion(versionConstraint.getVersion());
143         } else {
144             VersionRange.Bound lowerBound = versionConstraint.getRange().getLowerBound();
145             VersionRange.Bound upperBound = versionConstraint.getRange().getUpperBound();
146             if (lowerBound != null && lowerBound.equals(upperBound)) {
147                 result.addVersion(lowerBound.getVersion());
148             } else {
149                 Metadata.Nature wantedNature;
150                 String natureString = ConfigUtils.getString(
151                         session, request.getNature().name(), MAVEN_VERSION_RANGE_RESOLUTION_NATURE);
152                 if ("auto".equals(natureString)) {
153                     org.eclipse.aether.artifact.Artifact lowerArtifact = lowerBound != null
154                             ? request.getArtifact()
155                                     .setVersion(lowerBound.getVersion().toString())
156                             : null;
157                     org.eclipse.aether.artifact.Artifact upperArtifact = upperBound != null
158                             ? request.getArtifact()
159                                     .setVersion(upperBound.getVersion().toString())
160                             : null;
161 
162                     if (lowerArtifact != null && lowerArtifact.isSnapshot()
163                             || upperArtifact != null && upperArtifact.isSnapshot()) {
164                         wantedNature = Metadata.Nature.RELEASE_OR_SNAPSHOT;
165                     } else {
166                         wantedNature = Metadata.Nature.RELEASE;
167                     }
168                 } else {
169                     wantedNature = Metadata.Nature.valueOf(natureString.toUpperCase(Locale.ROOT));
170                 }
171 
172                 Map<String, ArtifactRepository> versionIndex = getVersions(session, result, request, wantedNature);
173 
174                 List<Version> versions = new ArrayList<>();
175                 for (Map.Entry<String, ArtifactRepository> v : versionIndex.entrySet()) {
176                     try {
177                         Version ver = versionScheme.parseVersion(v.getKey());
178                         if (versionConstraint.containsVersion(ver)) {
179                             versions.add(ver);
180                             result.setRepository(ver, v.getValue());
181                         }
182                     } catch (InvalidVersionSpecificationException e) {
183                         result.addException(e);
184                     }
185                 }
186 
187                 Collections.sort(versions);
188                 result.setVersions(versions);
189             }
190         }
191 
192         return result;
193     }
194 
195     private Map<String, ArtifactRepository> getVersions(
196             RepositorySystemSession session,
197             VersionRangeResult result,
198             VersionRangeRequest request,
199             Metadata.Nature wantedNature) {
200         RequestTrace trace = RequestTrace.newChild(request.getTrace(), request);
201 
202         Map<String, ArtifactRepository> versionIndex = new HashMap<>();
203 
204         Metadata metadata = new DefaultMetadata(
205                 request.getArtifact().getGroupId(),
206                 request.getArtifact().getArtifactId(),
207                 MAVEN_METADATA_XML,
208                 wantedNature);
209 
210         List<MetadataRequest> metadataRequests =
211                 new ArrayList<>(request.getRepositories().size());
212 
213         metadataRequests.add(new MetadataRequest(metadata, null, request.getRequestContext()));
214 
215         for (RemoteRepository repository : request.getRepositories()) {
216             MetadataRequest metadataRequest = new MetadataRequest(metadata, repository, request.getRequestContext());
217             metadataRequest.setDeleteLocalCopyIfMissing(true);
218             metadataRequest.setTrace(trace);
219             metadataRequests.add(metadataRequest);
220         }
221 
222         List<MetadataResult> metadataResults = metadataResolver.resolveMetadata(session, metadataRequests);
223 
224         WorkspaceReader workspace = session.getWorkspaceReader();
225         if (workspace != null) {
226             List<String> versions = workspace.findVersions(request.getArtifact());
227             for (String version : versions) {
228                 versionIndex.put(version, workspace.getRepository());
229             }
230         }
231 
232         for (MetadataResult metadataResult : metadataResults) {
233             result.addException(metadataResult.getException());
234 
235             ArtifactRepository repository = metadataResult.getRequest().getRepository();
236             if (repository == null) {
237                 repository = session.getLocalRepository();
238             }
239 
240             Versioning versioning = readVersions(session, trace, metadataResult.getMetadata(), repository, result);
241 
242             versioning = filterVersionsByRepositoryType(
243                     versioning, metadataResult.getRequest().getRepository());
244 
245             for (String version : versioning.getVersions()) {
246                 if (!versionIndex.containsKey(version)) {
247                     versionIndex.put(version, repository);
248                 }
249             }
250         }
251 
252         return versionIndex;
253     }
254 
255     private Versioning readVersions(
256             RepositorySystemSession session,
257             RequestTrace trace,
258             Metadata metadata,
259             ArtifactRepository repository,
260             VersionRangeResult result) {
261         Versioning versioning = null;
262         try {
263             if (metadata != null) {
264                 try (SyncContext syncContext = syncContextFactory.newInstance(session, true)) {
265                     syncContext.acquire(null, Collections.singleton(metadata));
266 
267                     if (metadata.getFile() != null && metadata.getFile().exists()) {
268                         try (InputStream in = new FileInputStream(metadata.getFile())) {
269                             Versioning parsed = new ValidatingMetadataXpp3Reader()
270                                     .read(in, false)
271                                     .getVersioning();
272 
273                             validateVersioning(parsed);
274 
275                             versioning = parsed;
276                         }
277                     }
278                 }
279             }
280         } catch (Exception e) {
281             invalidMetadata(session, trace, metadata, repository, e);
282             result.addException(e);
283         }
284 
285         return (versioning != null) ? versioning : new Versioning();
286     }
287 
288     /**
289      * Version tokens adopted from repository metadata must be valid coordinate components; metadata carrying
290      * anything else is treated as invalid.
291      */
292     private static void validateVersioning(Versioning versioning) throws IOException {
293         if (versioning == null) {
294             return;
295         }
296         for (String version : versioning.getVersions()) {
297             validateVersionToken(version);
298         }
299         validateVersionToken(versioning.getLatest());
300         validateVersionToken(versioning.getRelease());
301     }
302 
303     private static void validateVersionToken(String value) throws IOException {
304         if (value == null || value.isEmpty()) {
305             return;
306         }
307         boolean valid = !"..".equals(value);
308         if (valid) {
309             for (int i = 0; i < value.length(); i++) {
310                 char c = value.charAt(i);
311                 if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) {
312                     valid = false;
313                     break;
314                 }
315             }
316         }
317         if (!valid) {
318             throw new IOException("Metadata contains an invalid version token: '" + value + "'");
319         }
320     }
321 
322     private Versioning filterVersionsByRepositoryType(Versioning versioning, RemoteRepository remoteRepository) {
323         if (remoteRepository == null) {
324             return versioning;
325         }
326 
327         Versioning filteredVersions = versioning.clone();
328 
329         for (String version : versioning.getVersions()) {
330             if (!remoteRepository.getPolicy(ArtifactUtils.isSnapshot(version)).isEnabled()) {
331                 filteredVersions.removeVersion(version);
332             }
333         }
334 
335         return filteredVersions;
336     }
337 
338     private void invalidMetadata(
339             RepositorySystemSession session,
340             RequestTrace trace,
341             Metadata metadata,
342             ArtifactRepository repository,
343             Exception exception) {
344         RepositoryEvent.Builder event = new RepositoryEvent.Builder(session, EventType.METADATA_INVALID);
345         event.setTrace(trace);
346         event.setMetadata(metadata);
347         event.setException(exception);
348         event.setRepository(repository);
349 
350         repositoryEventDispatcher.dispatch(event.build());
351     }
352 }