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.internal.aether;
20  
21  import java.nio.file.Path;
22  import java.nio.file.Paths;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.LinkedHashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.function.Predicate;
31  import java.util.stream.Collectors;
32  
33  import org.apache.maven.api.di.Inject;
34  import org.apache.maven.api.di.Named;
35  import org.apache.maven.api.di.Singleton;
36  import org.apache.maven.api.services.TypeRegistry;
37  import org.apache.maven.api.xml.XmlNode;
38  import org.apache.maven.eventspy.internal.EventSpyDispatcher;
39  import org.apache.maven.execution.MavenExecutionRequest;
40  import org.apache.maven.internal.impl.resolver.MavenSessionBuilderSupplier;
41  import org.apache.maven.internal.xml.XmlNodeImpl;
42  import org.apache.maven.internal.xml.XmlPlexusConfiguration;
43  import org.apache.maven.model.ModelBase;
44  import org.apache.maven.resolver.RepositorySystemSessionFactory;
45  import org.apache.maven.rtinfo.RuntimeInformation;
46  import org.apache.maven.settings.Mirror;
47  import org.apache.maven.settings.Proxy;
48  import org.apache.maven.settings.Server;
49  import org.apache.maven.settings.building.SettingsProblem;
50  import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
51  import org.apache.maven.settings.crypto.SettingsDecrypter;
52  import org.apache.maven.settings.crypto.SettingsDecryptionResult;
53  import org.codehaus.plexus.configuration.PlexusConfiguration;
54  import org.eclipse.aether.*;
55  import org.eclipse.aether.RepositorySystemSession.SessionBuilder;
56  import org.eclipse.aether.artifact.Artifact;
57  import org.eclipse.aether.artifact.DefaultArtifact;
58  import org.eclipse.aether.collection.VersionFilter;
59  import org.eclipse.aether.repository.RepositoryPolicy;
60  import org.eclipse.aether.resolution.ResolutionErrorPolicy;
61  import org.eclipse.aether.util.graph.version.*;
62  import org.eclipse.aether.util.listener.ChainedRepositoryListener;
63  import org.eclipse.aether.util.repository.AuthenticationBuilder;
64  import org.eclipse.aether.util.repository.ChainedLocalRepositoryManager;
65  import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
66  import org.eclipse.aether.util.repository.DefaultMirrorSelector;
67  import org.eclipse.aether.util.repository.DefaultProxySelector;
68  import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy;
69  import org.eclipse.aether.util.repository.SimpleResolutionErrorPolicy;
70  import org.eclipse.aether.version.InvalidVersionSpecificationException;
71  import org.eclipse.aether.version.Version;
72  import org.eclipse.aether.version.VersionRange;
73  import org.eclipse.aether.version.VersionScheme;
74  import org.slf4j.Logger;
75  import org.slf4j.LoggerFactory;
76  
77  import static java.util.Objects.requireNonNull;
78  
79  /**
80   * @since 3.3.0
81   */
82  @Named
83  @Singleton
84  public class DefaultRepositorySystemSessionFactory implements RepositorySystemSessionFactory {
85      /**
86       * User property for version filters expression, a semicolon separated list of filters to apply. By default, no version
87       * filter is applied (like in Maven 3).
88       * <p>
89       * Supported filters:
90       * <ul>
91       *     <li>"h" or "h(num)" - highest version or top list of highest ones filter</li>
92       *     <li>"l" or "l(num)" - lowest version or bottom list of lowest ones filter</li>
93       *     <li>"s" - contextual snapshot filter</li>
94       *     <li>"e(G:A:V)" - predicate filter (leaves out G:A:V from range, if hit, V can be range)</li>
95       * </ul>
96       * Example filter expression: {@code "h(5);s;e(org.foo:bar:1)} will cause: ranges are filtered for "top 5" (instead
97       * full range), snapshots are banned if root project is not a snapshot, and if range for {@code org.foo:bar} is
98       * being processed, version 1 is omitted.
99       *
100      * @since 4.0.0
101      */
102     private static final String MAVEN_VERSION_FILTERS = "maven.versionFilters";
103 
104     /**
105      * User property for chained LRM: list of "tail" local repository paths (separated by comma), to be used with
106      * {@link ChainedLocalRepositoryManager}.
107      * Default value: {@code null}, no chained LRM is used.
108      *
109      * @since 3.9.0
110      */
111     private static final String MAVEN_REPO_LOCAL_TAIL = "maven.repo.local.tail";
112 
113     /**
114      * User property for reverse dependency tree. If enabled, Maven will record ".tracking" directory into local
115      * repository with "reverse dependency tree", essentially explaining WHY given artifact is present in local
116      * repository.
117      * Default: {@code false}, will not record anything.
118      *
119      * @since 3.9.0
120      */
121     private static final String MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE = "maven.repo.local.recordReverseTree";
122 
123     /**
124      * User property for selecting dependency manager behaviour regarding transitive dependencies and dependency
125      * management entries in their POMs. Maven 3 targeted full backward compatibility with Maven2, hence it ignored
126      * dependency management entries in transitive dependency POMs. Maven 4 enables "transitivity" by default, hence
127      * unlike Maven2, obeys dependency management entries deep in dependency graph as well.
128      * <p>
129      * Default: {@code "true"}.
130      *
131      * @since 4.0.0
132      */
133     private static final String MAVEN_RESOLVER_DEPENDENCY_MANAGER_TRANSITIVITY_KEY =
134             "maven.resolver.dependencyManagerTransitivity";
135 
136     private static final String MAVEN_RESOLVER_TRANSPORT_KEY = "maven.resolver.transport";
137 
138     private static final String MAVEN_RESOLVER_TRANSPORT_DEFAULT = "default";
139 
140     private static final String MAVEN_RESOLVER_TRANSPORT_WAGON = "wagon";
141 
142     private static final String MAVEN_RESOLVER_TRANSPORT_APACHE = "apache";
143 
144     private static final String MAVEN_RESOLVER_TRANSPORT_JDK = "jdk";
145 
146     /**
147      * This name for Apache HttpClient transport is deprecated.
148      *
149      * @deprecated Renamed to {@link #MAVEN_RESOLVER_TRANSPORT_APACHE}
150      */
151     @Deprecated
152     private static final String MAVEN_RESOLVER_TRANSPORT_NATIVE = "native";
153 
154     private static final String MAVEN_RESOLVER_TRANSPORT_AUTO = "auto";
155 
156     private static final String WAGON_TRANSPORTER_PRIORITY_KEY = "aether.priority.WagonTransporterFactory";
157 
158     private static final String APACHE_HTTP_TRANSPORTER_PRIORITY_KEY = "aether.priority.ApacheTransporterFactory";
159 
160     private static final String JDK_HTTP_TRANSPORTER_PRIORITY_KEY = "aether.priority.JdkTransporterFactory";
161 
162     private static final String FILE_TRANSPORTER_PRIORITY_KEY = "aether.priority.FileTransporterFactory";
163 
164     private static final String RESOLVER_MAX_PRIORITY = String.valueOf(Float.MAX_VALUE);
165 
166     private final Logger logger = LoggerFactory.getLogger(getClass());
167 
168     private final RepositorySystem repoSystem;
169 
170     private final SettingsDecrypter settingsDecrypter;
171 
172     private final EventSpyDispatcher eventSpyDispatcher;
173 
174     private final RuntimeInformation runtimeInformation;
175 
176     private final TypeRegistry typeRegistry;
177 
178     private final VersionScheme versionScheme;
179 
180     private final Map<String, MavenExecutionRequestExtender> requestExtenders;
181 
182     private final Map<String, RepositorySystemSessionExtender> sessionExtenders;
183 
184     @SuppressWarnings("checkstyle:ParameterNumber")
185     @Inject
186     DefaultRepositorySystemSessionFactory(
187             RepositorySystem repoSystem,
188             SettingsDecrypter settingsDecrypter,
189             EventSpyDispatcher eventSpyDispatcher,
190             RuntimeInformation runtimeInformation,
191             TypeRegistry typeRegistry,
192             VersionScheme versionScheme,
193             Map<String, MavenExecutionRequestExtender> requestExtenders,
194             Map<String, RepositorySystemSessionExtender> sessionExtenders) {
195         this.repoSystem = repoSystem;
196         this.settingsDecrypter = settingsDecrypter;
197         this.eventSpyDispatcher = eventSpyDispatcher;
198         this.runtimeInformation = runtimeInformation;
199         this.typeRegistry = typeRegistry;
200         this.versionScheme = versionScheme;
201         this.requestExtenders = requestExtenders;
202         this.sessionExtenders = sessionExtenders;
203     }
204 
205     @Deprecated
206     public RepositorySystemSession newRepositorySession(MavenExecutionRequest request) {
207         return newRepositorySessionBuilder(request).build();
208     }
209 
210     @SuppressWarnings({"checkstyle:methodLength"})
211     public SessionBuilder newRepositorySessionBuilder(MavenExecutionRequest request) {
212         requireNonNull(request, "request");
213 
214         // apply MavenExecutionRequestExtenders
215         for (MavenExecutionRequestExtender requestExtender : requestExtenders.values()) {
216             requestExtender.extend(request);
217         }
218 
219         MavenSessionBuilderSupplier supplier = new MavenSessionBuilderSupplier(repoSystem);
220         SessionBuilder sessionBuilder = supplier.get();
221         sessionBuilder.setArtifactTypeRegistry(new TypeRegistryAdapter(typeRegistry)); // dynamic
222         sessionBuilder.setCache(request.getRepositoryCache());
223 
224         // this map is read ONLY to get config from (profiles + env + system + user)
225         Map<String, String> mergedProps = createMergedProperties(request);
226 
227         // configProps map is kept "pristine", is written ONLY, the mandatory resolver config
228         Map<String, Object> configProps = new LinkedHashMap<>();
229         configProps.put(ConfigurationProperties.USER_AGENT, getUserAgent());
230         configProps.put(ConfigurationProperties.INTERACTIVE, request.isInteractiveMode());
231         configProps.put("maven.startTime", request.getStartTime());
232 
233         sessionBuilder.setOffline(request.isOffline());
234         sessionBuilder.setChecksumPolicy(request.getGlobalChecksumPolicy());
235         sessionBuilder.setUpdatePolicy(
236                 request.isNoSnapshotUpdates()
237                         ? RepositoryPolicy.UPDATE_POLICY_NEVER
238                         : request.isUpdateSnapshots() ? RepositoryPolicy.UPDATE_POLICY_ALWAYS : null);
239 
240         int errorPolicy = 0;
241         errorPolicy |= request.isCacheNotFound()
242                 ? ResolutionErrorPolicy.CACHE_NOT_FOUND
243                 : ResolutionErrorPolicy.CACHE_DISABLED;
244         errorPolicy |= request.isCacheTransferError()
245                 ? ResolutionErrorPolicy.CACHE_TRANSFER_ERROR
246                 : ResolutionErrorPolicy.CACHE_DISABLED;
247         sessionBuilder.setResolutionErrorPolicy(
248                 new SimpleResolutionErrorPolicy(errorPolicy, errorPolicy | ResolutionErrorPolicy.CACHE_NOT_FOUND));
249 
250         sessionBuilder.setArtifactDescriptorPolicy(new SimpleArtifactDescriptorPolicy(
251                 request.isIgnoreMissingArtifactDescriptor(), request.isIgnoreInvalidArtifactDescriptor()));
252 
253         VersionFilter versionFilter = buildVersionFilter(mergedProps.get(MAVEN_VERSION_FILTERS));
254         if (versionFilter != null) {
255             sessionBuilder.setVersionFilter(versionFilter);
256         }
257 
258         DefaultSettingsDecryptionRequest decrypt = new DefaultSettingsDecryptionRequest();
259         decrypt.setProxies(request.getProxies());
260         decrypt.setServers(request.getServers());
261         SettingsDecryptionResult decrypted = settingsDecrypter.decrypt(decrypt);
262 
263         if (logger.isDebugEnabled()) {
264             for (SettingsProblem problem : decrypted.getProblems()) {
265                 logger.debug(problem.getMessage(), problem.getException());
266             }
267         }
268 
269         DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
270         for (Mirror mirror : request.getMirrors()) {
271             mirrorSelector.add(
272                     mirror.getId(),
273                     mirror.getUrl(),
274                     mirror.getLayout(),
275                     false,
276                     mirror.isBlocked(),
277                     mirror.getMirrorOf(),
278                     mirror.getMirrorOfLayouts());
279         }
280         sessionBuilder.setMirrorSelector(mirrorSelector);
281 
282         DefaultProxySelector proxySelector = new DefaultProxySelector();
283         for (Proxy proxy : decrypted.getProxies()) {
284             AuthenticationBuilder authBuilder = new AuthenticationBuilder();
285             authBuilder.addUsername(proxy.getUsername()).addPassword(proxy.getPassword());
286             proxySelector.add(
287                     new org.eclipse.aether.repository.Proxy(
288                             proxy.getProtocol(), proxy.getHost(), proxy.getPort(), authBuilder.build()),
289                     proxy.getNonProxyHosts());
290         }
291         sessionBuilder.setProxySelector(proxySelector);
292 
293         // Note: we do NOT use WagonTransportConfigurationKeys here as Maven Core does NOT depend on Wagon Transport
294         // and this is okay and "good thing".
295         DefaultAuthenticationSelector authSelector = new DefaultAuthenticationSelector();
296         for (Server server : decrypted.getServers()) {
297             AuthenticationBuilder authBuilder = new AuthenticationBuilder();
298             authBuilder.addUsername(server.getUsername()).addPassword(server.getPassword());
299             authBuilder.addPrivateKey(server.getPrivateKey(), server.getPassphrase());
300             authSelector.add(server.getId(), authBuilder.build());
301 
302             if (server.getConfiguration() != null) {
303                 XmlNode dom = server.getDelegate().getConfiguration();
304                 List<XmlNode> children = dom.getChildren().stream()
305                         .filter(c -> !"wagonProvider".equals(c.getName()))
306                         .collect(Collectors.toList());
307                 dom = new XmlNodeImpl(dom.getName(), null, null, children, null);
308                 PlexusConfiguration config = XmlPlexusConfiguration.toPlexusConfiguration(dom);
309                 configProps.put("aether.transport.wagon.config." + server.getId(), config);
310 
311                 // Translate to proper resolver configuration properties as well (as Plexus XML above is Wagon specific
312                 // only) but support only configuration/httpConfiguration/all, see
313                 // https://maven.apache.org/guides/mini/guide-http-settings.html
314                 Map<String, String> headers = null;
315                 Integer connectTimeout = null;
316                 Integer requestTimeout = null;
317 
318                 PlexusConfiguration httpHeaders = config.getChild("httpHeaders", false);
319                 if (httpHeaders != null) {
320                     PlexusConfiguration[] properties = httpHeaders.getChildren("property");
321                     if (properties != null && properties.length > 0) {
322                         headers = new HashMap<>();
323                         for (PlexusConfiguration property : properties) {
324                             headers.put(
325                                     property.getChild("name").getValue(),
326                                     property.getChild("value").getValue());
327                         }
328                     }
329                 }
330 
331                 PlexusConfiguration connectTimeoutXml = config.getChild("connectTimeout", false);
332                 if (connectTimeoutXml != null) {
333                     connectTimeout = Integer.parseInt(connectTimeoutXml.getValue());
334                 } else {
335                     // fallback configuration name
336                     PlexusConfiguration httpConfiguration = config.getChild("httpConfiguration", false);
337                     if (httpConfiguration != null) {
338                         PlexusConfiguration httpConfigurationAll = httpConfiguration.getChild("all", false);
339                         if (httpConfigurationAll != null) {
340                             connectTimeoutXml = httpConfigurationAll.getChild("connectionTimeout", false);
341                             if (connectTimeoutXml != null) {
342                                 connectTimeout = Integer.parseInt(connectTimeoutXml.getValue());
343                                 logger.warn("Settings for server {} uses legacy format", server.getId());
344                             }
345                         }
346                     }
347                 }
348 
349                 PlexusConfiguration requestTimeoutXml = config.getChild("requestTimeout", false);
350                 if (requestTimeoutXml != null) {
351                     requestTimeout = Integer.parseInt(requestTimeoutXml.getValue());
352                 } else {
353                     // fallback configuration name
354                     PlexusConfiguration httpConfiguration = config.getChild("httpConfiguration", false);
355                     if (httpConfiguration != null) {
356                         PlexusConfiguration httpConfigurationAll = httpConfiguration.getChild("all", false);
357                         if (httpConfigurationAll != null) {
358                             requestTimeoutXml = httpConfigurationAll.getChild("readTimeout", false);
359                             if (requestTimeoutXml != null) {
360                                 requestTimeout = Integer.parseInt(requestTimeoutXml.getValue());
361                                 logger.warn("Settings for server {} uses legacy format", server.getId());
362                             }
363                         }
364                     }
365                 }
366 
367                 // org.eclipse.aether.ConfigurationProperties.HTTP_HEADERS => Map<String, String>
368                 if (headers != null) {
369                     configProps.put(ConfigurationProperties.HTTP_HEADERS + "." + server.getId(), headers);
370                 }
371                 // org.eclipse.aether.ConfigurationProperties.CONNECT_TIMEOUT => int
372                 if (connectTimeout != null) {
373                     configProps.put(ConfigurationProperties.CONNECT_TIMEOUT + "." + server.getId(), connectTimeout);
374                 }
375                 // org.eclipse.aether.ConfigurationProperties.REQUEST_TIMEOUT => int
376                 if (requestTimeout != null) {
377                     configProps.put(ConfigurationProperties.REQUEST_TIMEOUT + "." + server.getId(), requestTimeout);
378                 }
379             }
380 
381             configProps.put("aether.transport.wagon.perms.fileMode." + server.getId(), server.getFilePermissions());
382             configProps.put("aether.transport.wagon.perms.dirMode." + server.getId(), server.getDirectoryPermissions());
383         }
384         sessionBuilder.setAuthenticationSelector(authSelector);
385 
386         Object transport = mergedProps.getOrDefault(MAVEN_RESOLVER_TRANSPORT_KEY, MAVEN_RESOLVER_TRANSPORT_DEFAULT);
387         if (MAVEN_RESOLVER_TRANSPORT_DEFAULT.equals(transport)) {
388             // The "default" mode (user did not set anything) from now on defaults to AUTO
389         } else if (MAVEN_RESOLVER_TRANSPORT_JDK.equals(transport)) {
390             // Make sure (whatever extra priority is set) that resolver file/jdk is selected
391             configProps.put(FILE_TRANSPORTER_PRIORITY_KEY, RESOLVER_MAX_PRIORITY);
392             configProps.put(JDK_HTTP_TRANSPORTER_PRIORITY_KEY, RESOLVER_MAX_PRIORITY);
393         } else if (MAVEN_RESOLVER_TRANSPORT_APACHE.equals(transport)
394                 || MAVEN_RESOLVER_TRANSPORT_NATIVE.equals(transport)) {
395             if (MAVEN_RESOLVER_TRANSPORT_NATIVE.equals(transport)) {
396                 logger.warn(
397                         "Transport name '{}' is DEPRECATED/RENAMED, use '{}' instead",
398                         MAVEN_RESOLVER_TRANSPORT_NATIVE,
399                         MAVEN_RESOLVER_TRANSPORT_APACHE);
400             }
401             // Make sure (whatever extra priority is set) that resolver file/apache is selected
402             configProps.put(FILE_TRANSPORTER_PRIORITY_KEY, RESOLVER_MAX_PRIORITY);
403             configProps.put(APACHE_HTTP_TRANSPORTER_PRIORITY_KEY, RESOLVER_MAX_PRIORITY);
404         } else if (MAVEN_RESOLVER_TRANSPORT_WAGON.equals(transport)) {
405             // Make sure (whatever extra priority is set) that wagon is selected
406             configProps.put(WAGON_TRANSPORTER_PRIORITY_KEY, RESOLVER_MAX_PRIORITY);
407         } else if (!MAVEN_RESOLVER_TRANSPORT_AUTO.equals(transport)) {
408             throw new IllegalArgumentException("Unknown resolver transport '" + transport
409                     + "'. Supported transports are: " + MAVEN_RESOLVER_TRANSPORT_WAGON + ", "
410                     + MAVEN_RESOLVER_TRANSPORT_APACHE + ", " + MAVEN_RESOLVER_TRANSPORT_JDK + ", "
411                     + MAVEN_RESOLVER_TRANSPORT_AUTO);
412         }
413 
414         sessionBuilder.setIgnoreArtifactDescriptorRepositories(request.isIgnoreTransitiveRepositories());
415 
416         sessionBuilder.setTransferListener(request.getTransferListener());
417 
418         RepositoryListener repositoryListener = eventSpyDispatcher.chainListener(new LoggingRepositoryListener(logger));
419 
420         boolean recordReverseTree = Boolean.parseBoolean(
421                 mergedProps.getOrDefault(MAVEN_REPO_LOCAL_RECORD_REVERSE_TREE, Boolean.FALSE.toString()));
422         if (recordReverseTree) {
423             repositoryListener = new ChainedRepositoryListener(repositoryListener, new ReverseTreeRepositoryListener());
424         }
425         sessionBuilder.setRepositoryListener(repositoryListener);
426 
427         // may be overridden
428         String resolverDependencyManagerTransitivity =
429                 mergedProps.getOrDefault(MAVEN_RESOLVER_DEPENDENCY_MANAGER_TRANSITIVITY_KEY, Boolean.TRUE.toString());
430         sessionBuilder.setDependencyManager(
431                 supplier.getDependencyManager(Boolean.parseBoolean(resolverDependencyManagerTransitivity)));
432 
433         ArrayList<Path> paths = new ArrayList<>();
434         paths.add(Paths.get(request.getLocalRepository().getBasedir()));
435         String localRepoTail = mergedProps.get(MAVEN_REPO_LOCAL_TAIL);
436         if (localRepoTail != null) {
437             Arrays.stream(localRepoTail.split(","))
438                     .filter(p -> p != null && !p.trim().isEmpty())
439                     .map(Paths::get)
440                     .forEach(paths::add);
441         }
442         sessionBuilder.withLocalRepositoryBaseDirectories(paths);
443 
444         for (RepositorySystemSessionExtender extender : sessionExtenders.values()) {
445             extender.extend(request, configProps, mirrorSelector, proxySelector, authSelector);
446         }
447 
448         // at this point we have "config" with pure MANDATORY resolver config, so resolver final config properties are
449         // mergedProperties + configProperties
450         HashMap<String, Object> finalConfigProperties = new HashMap<>();
451         finalConfigProperties.putAll(mergedProps);
452         finalConfigProperties.putAll(configProps);
453 
454         sessionBuilder.setUserProperties(request.getUserProperties());
455         sessionBuilder.setSystemProperties(request.getSystemProperties());
456         sessionBuilder.setConfigProperties(finalConfigProperties);
457 
458         return sessionBuilder;
459     }
460 
461     private VersionFilter buildVersionFilter(String filterExpression) {
462         ArrayList<VersionFilter> filters = new ArrayList<>();
463         if (filterExpression != null) {
464             List<String> expressions = Arrays.stream(filterExpression.split(";"))
465                     .filter(s -> s != null && !s.trim().isEmpty())
466                     .toList();
467             for (String expression : expressions) {
468                 if ("h".equals(expression)) {
469                     filters.add(new HighestVersionFilter());
470                 } else if (expression.startsWith("h(") && expression.endsWith(")")) {
471                     int num = Integer.parseInt(expression.substring(2, expression.length() - 1));
472                     filters.add(new HighestVersionFilter(num));
473                 } else if ("l".equals(expression)) {
474                     filters.add(new LowestVersionFilter());
475                 } else if (expression.startsWith("l(") && expression.endsWith(")")) {
476                     int num = Integer.parseInt(expression.substring(2, expression.length() - 1));
477                     filters.add(new LowestVersionFilter(num));
478                 } else if ("s".equals(expression)) {
479                     filters.add(new ContextualSnapshotVersionFilter());
480                 } else if (expression.startsWith("e(") && expression.endsWith(")")) {
481                     Artifact artifact = new DefaultArtifact(expression.substring(2, expression.length() - 1));
482                     VersionRange versionRange =
483                             artifact.getVersion().contains(",") ? parseVersionRange(artifact.getVersion()) : null;
484                     Predicate<Artifact> predicate = a -> {
485                         if (artifact.getGroupId().equals(a.getGroupId())
486                                 && artifact.getArtifactId().equals(a.getArtifactId())) {
487                             if (versionRange != null) {
488                                 Version v = parseVersion(a.getVersion());
489                                 return !versionRange.containsVersion(v);
490                             } else {
491                                 return !artifact.getVersion().equals(a.getVersion());
492                             }
493                         }
494                         return true;
495                     };
496                     filters.add(new PredicateVersionFilter(predicate));
497                 } else {
498                     throw new IllegalArgumentException("Unsupported filter expression: " + expression);
499                 }
500             }
501         }
502         if (filters.isEmpty()) {
503             return null;
504         } else if (filters.size() == 1) {
505             return filters.get(0);
506         } else {
507             return ChainedVersionFilter.newInstance(filters);
508         }
509     }
510 
511     private Version parseVersion(String spec) {
512         try {
513             return versionScheme.parseVersion(spec);
514         } catch (InvalidVersionSpecificationException e) {
515             throw new RuntimeException(e);
516         }
517     }
518 
519     private VersionRange parseVersionRange(String spec) {
520         try {
521             return versionScheme.parseVersionRange(spec);
522         } catch (InvalidVersionSpecificationException e) {
523             throw new RuntimeException(e);
524         }
525     }
526 
527     @SuppressWarnings({"unchecked", "rawtypes"})
528     private Map<String, String> createMergedProperties(MavenExecutionRequest request) {
529         // this throwaway map is really ONLY to get config from (profiles + env + system + user)
530         Map<String, String> mergedProps = new HashMap<>();
531         mergedProps.putAll(getPropertiesFromRequestedProfiles(request));
532         mergedProps.putAll(new HashMap<String, String>((Map) request.getSystemProperties()));
533         mergedProps.putAll(new HashMap<String, String>((Map) request.getUserProperties()));
534         return mergedProps;
535     }
536 
537     private Map<String, String> getPropertiesFromRequestedProfiles(MavenExecutionRequest request) {
538         HashSet<String> activeProfileId =
539                 new HashSet<>(request.getProfileActivation().getRequiredActiveProfileIds());
540         activeProfileId.addAll(request.getProfileActivation().getOptionalActiveProfileIds());
541 
542         return request.getProfiles().stream()
543                 .filter(profile -> activeProfileId.contains(profile.getId()))
544                 .map(ModelBase::getProperties)
545                 .flatMap(properties -> properties.entrySet().stream())
546                 .filter(e -> e.getValue() != null)
547                 .collect(Collectors.toMap(
548                         e -> String.valueOf(e.getKey()), e -> String.valueOf(e.getValue()), (k1, k2) -> k2));
549     }
550 
551     private String getUserAgent() {
552         String version = runtimeInformation.getMavenVersion();
553         version = version.isEmpty() ? version : "/" + version;
554         return "Apache-Maven" + version + " (Java " + System.getProperty("java.version") + "; "
555                 + System.getProperty("os.name") + " " + System.getProperty("os.version") + ")";
556     }
557 }