View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package de.softwareforge.testing.maven;
16  
17  import static java.lang.String.format;
18  import static java.util.Objects.requireNonNull;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Objects;
27  import java.util.SortedSet;
28  import java.util.TreeSet;
29  
30  import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
31  import org.apache.maven.settings.Profile;
32  import org.apache.maven.settings.Repository;
33  import org.apache.maven.settings.Settings;
34  import org.apache.maven.settings.building.DefaultSettingsBuilder;
35  import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
36  import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
37  import org.apache.maven.settings.building.SettingsBuildingException;
38  import org.apache.maven.settings.building.SettingsBuildingRequest;
39  import org.apache.maven.settings.building.SettingsBuildingResult;
40  import org.eclipse.aether.DefaultRepositorySystemSession;
41  import org.eclipse.aether.RepositoryException;
42  import org.eclipse.aether.RepositorySystem;
43  import org.eclipse.aether.RepositorySystemSession;
44  import org.eclipse.aether.artifact.Artifact;
45  import org.eclipse.aether.artifact.DefaultArtifact;
46  import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
47  import org.eclipse.aether.impl.DefaultServiceLocator;
48  import org.eclipse.aether.repository.LocalRepository;
49  import org.eclipse.aether.repository.RemoteRepository;
50  import org.eclipse.aether.resolution.ArtifactRequest;
51  import org.eclipse.aether.resolution.ArtifactResult;
52  import org.eclipse.aether.resolution.VersionRangeRequest;
53  import org.eclipse.aether.resolution.VersionRangeResolutionException;
54  import org.eclipse.aether.resolution.VersionRangeResult;
55  import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
56  import org.eclipse.aether.spi.connector.transport.TransporterFactory;
57  import org.eclipse.aether.spi.locator.ServiceLocator;
58  import org.eclipse.aether.transport.file.FileTransporterFactory;
59  import org.eclipse.aether.transport.http.HttpTransporterFactory;
60  import org.eclipse.aether.version.Version;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /**
65   * A quick and dirty artifact loader. Downloads published artifacts from the Maven repository system.
66   * <p>
67   * The loader respects the local maven settings (repositories, mirrors etc.) if present. If no configuration is found, a hard-coded reference to <a
68   * href="https://repo.maven.apache.org/maven2/">Maven Central</a> is used.
69   */
70  public final class MavenArtifactLoader {
71  
72      private static final Logger LOG = LoggerFactory.getLogger(MavenArtifactLoader.class);
73  
74      static final RemoteRepository CENTRAL_REPO = new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build();
75  
76      private static final String USER_HOME = System.getProperty("user.home");
77      private static final File USER_MAVEN_HOME = new File(USER_HOME, ".m2");
78      private static final String ENV_M2_HOME = System.getenv("M2_HOME");
79  
80      private static final File DEFAULT_USER_SETTINGS_FILE = new File(USER_MAVEN_HOME, "settings.xml");
81      private static final File DEFAULT_USER_REPOSITORY = new File(USER_MAVEN_HOME, "repository");
82      private static final File DEFAULT_GLOBAL_SETTINGS_FILE =
83              new File(System.getProperty("maven.home", Objects.requireNonNullElse(ENV_M2_HOME, "")), "conf/settings.xml");
84  
85      private final RepositorySystem repositorySystem;
86      private final RepositorySystemSession mavenSession;
87      private final List<RemoteRepository> remoteRepositories;
88  
89      private final String extension;
90  
91      /**
92       * Creates a new artifact loader for 'jar' artifacts.
93       */
94      public MavenArtifactLoader() {
95          this("jar");
96      }
97  
98      /**
99       * Creates a new artifact loader for artifacts.
100      *
101      * @param extension The artifact extension. Must not be null.
102      */
103     public MavenArtifactLoader(String extension) {
104         this(extension, null);
105     }
106 
107     MavenArtifactLoader(String extension, List<RemoteRepository> remoteRepositoriesForTesting) {
108         this.extension = requireNonNull(extension, "extension is null");
109 
110         @SuppressWarnings("deprecation")
111         ServiceLocator serviceLocator = createServiceLocator();
112         this.repositorySystem = serviceLocator.getService(RepositorySystem.class);
113 
114         try {
115             Settings settings = createSettings();
116             File localRepositoryLocation = settings.getLocalRepository() != null ? new File(settings.getLocalRepository()) : DEFAULT_USER_REPOSITORY;
117             LocalRepository localRepository = new LocalRepository(localRepositoryLocation);
118 
119             if (remoteRepositoriesForTesting != null) {
120                 this.remoteRepositories = remoteRepositoriesForTesting;
121             } else {
122                 this.remoteRepositories = extractRemoteRepositories(settings);
123             }
124 
125             DefaultRepositorySystemSession mavenSession = MavenRepositorySystemUtils.newSession();
126 
127             this.mavenSession = mavenSession.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(mavenSession, localRepository));
128 
129         } catch (SettingsBuildingException e) {
130             throw new IllegalStateException("Could not load maven settings:", e);
131         }
132     }
133 
134     /**
135      * Create a new version match builder to retrieve an artifact.
136      *
137      * @param groupId    The Apache Maven Group Id. Must not be null.
138      * @param artifactId The Apache Maven Artifact Id. Must not be null.
139      * @return A {@link MavenVersionMatchBuilder} instance
140      */
141     public MavenVersionMatchBuilder builder(String groupId, String artifactId) {
142         requireNonNull(groupId, "groupId is null");
143         requireNonNull(artifactId, "artifactId is null");
144 
145         return new MavenVersionMatchBuilder(this, groupId, artifactId);
146     }
147 
148     /**
149      * Download an artifact file from the Maven repository system.
150      *
151      * @param groupId    The Apache Maven Group Id. Must not be null.
152      * @param artifactId The Apache Maven Artifact Id. Must not be null.
153      * @param version    The Apache Maven Artifact version. Must not be null.
154      * @return A file representing a successfully downloaded artifact.
155      * @throws IOException If the artifact could not be found or an IO problem happened while locating or downloading the artifact.
156      */
157     public File getArtifactFile(String groupId, String artifactId, String version) throws IOException {
158         requireNonNull(groupId, "groupId is null");
159         requireNonNull(artifactId, "artifactId is null");
160         requireNonNull(version, "version is null");
161 
162         ArtifactRequest artifactRequest = new ArtifactRequest();
163         artifactRequest.setArtifact(new DefaultArtifact(groupId, artifactId, extension, version));
164         artifactRequest.setRepositories(this.remoteRepositories);
165         try {
166             ArtifactResult artifactResult = this.repositorySystem.resolveArtifact(mavenSession, artifactRequest);
167             Artifact artifact = artifactResult.getArtifact();
168             return artifact.getFile();
169         } catch (RepositoryException e) {
170             throw new IOException(e);
171         }
172     }
173 
174     /**
175      * Find a matching artifact version from a partially defined artifact version.
176      * <p>
177      * Any located artifact in the repository system is compared to the version given.
178      * <br>
179      * Using this method is equivalent to calling
180      * <pre>
181      *  builder(groupId, artifactId)
182      *                 .partialMatch(version)
183      *                 .extension(extension)
184      *                 .includeSnapshots(true)
185      *                 .findBestMatch();
186      * </pre>
187      *
188      * but will throw an IOException if no version could be found.
189      *
190      * @param groupId    The Apache Maven Group Id. Must not be null.
191      * @param artifactId The Apache Maven Artifact Id. Must not be null.
192      * @param version    A partial version string. Must not be null. An empty string matches any version.
193      * @return The latest version that matches the partial version string. It either starts with the partial version string given (an empty version string
194      * matches any version) or is exactly the provided version.
195      * @throws IOException If an IO problem happened during artifact download or no versions were found during resolution.
196      */
197     public String findLatestVersion(String groupId, String artifactId, String version) throws IOException {
198         requireNonNull(groupId, "groupId is null");
199         requireNonNull(artifactId, "artifactId is null");
200         requireNonNull(version, "version is null");
201         return builder(groupId, artifactId)
202                 .partialMatch(version)
203                 .extension(extension)
204                 .includeSnapshots(true)
205                 .findBestMatch()
206                 .orElseThrow(() -> new IOException(format("No suitable candidate for %s:%s:%s found!", groupId, artifactId, version)));
207     }
208 
209     SortedSet<Version> findAllVersions(MavenVersionMatchBuilder builder) throws IOException {
210 
211         Artifact artifact = new DefaultArtifact(builder.groupId(), builder.artifactId(), builder.extension(), "[0,)");
212 
213         VersionRangeRequest rangeRequest = new VersionRangeRequest();
214         rangeRequest.setArtifact(artifact);
215         rangeRequest.setRepositories(this.remoteRepositories);
216 
217         try {
218             VersionRangeResult rangeResult = this.repositorySystem.resolveVersionRange(mavenSession, rangeRequest);
219             SortedSet<Version> resultBuilder = new TreeSet<>();
220             List<Version> artifactVersions = rangeResult.getVersions();
221             VersionStrategy versionStrategy = builder.versionStrategy();
222             if (artifactVersions != null) {
223                 for (Version artifactVersion : artifactVersions) {
224                     boolean isSnapshot = artifactVersion.toString().endsWith("-SNAPSHOT");
225                     boolean match = versionStrategy.matchVersion(artifactVersion);
226 
227                     // remove match if snapshots are not requested but the version is a snapshot
228                     if (isSnapshot) {
229                         match &= builder.includeSnapshots();
230                     }
231 
232                     if (match) {
233                         resultBuilder.add(artifactVersion);
234                     }
235                 }
236             }
237             return Collections.unmodifiableSortedSet(resultBuilder);
238         } catch (VersionRangeResolutionException e) {
239             throw new IOException(format("Could not resolve version range: %s", rangeRequest), e);
240         }
241     }
242 
243 
244     @SuppressWarnings("deprecation")
245     private static ServiceLocator createServiceLocator() {
246         DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
247 
248         locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
249         locator.addService(TransporterFactory.class, FileTransporterFactory.class);
250         locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
251 
252         locator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() {
253             @Override
254             public void serviceCreationFailed(Class<?> type, Class<?> impl, Throwable e) {
255                 LOG.error(format("Could not create instance of %s (implementation %s): ", type.getSimpleName(), impl.getSimpleName()), e);
256             }
257         });
258 
259         return locator;
260     }
261 
262     private static Settings createSettings() throws SettingsBuildingException {
263         SettingsBuildingRequest settingsBuildingRequest = new DefaultSettingsBuildingRequest()
264                 .setSystemProperties(System.getProperties())
265                 .setUserSettingsFile(DEFAULT_USER_SETTINGS_FILE)
266                 .setGlobalSettingsFile(DEFAULT_GLOBAL_SETTINGS_FILE);
267 
268         DefaultSettingsBuilderFactory settingBuilderFactory = new DefaultSettingsBuilderFactory();
269         DefaultSettingsBuilder settingsBuilder = settingBuilderFactory.newInstance();
270         SettingsBuildingResult settingsBuildingResult = settingsBuilder.build(settingsBuildingRequest);
271 
272         return settingsBuildingResult.getEffectiveSettings();
273     }
274 
275     private static List<RemoteRepository> extractRemoteRepositories(Settings settings) {
276         Map<String, Profile> profiles = settings.getProfilesAsMap();
277         List<RemoteRepository> builder = new ArrayList<>();
278 
279         boolean foundRepository = false;
280         for (String profileName : settings.getActiveProfiles()) {
281             Profile profile = profiles.get(profileName);
282             if (profile != null) {
283                 List<Repository> repositories = profile.getRepositories();
284                 if (repositories != null) {
285                     for (Repository repo : repositories) {
286                         builder.add(new RemoteRepository.Builder(repo.getId(), "default", repo.getUrl()).build());
287                         foundRepository = true;
288                     }
289                 }
290             }
291         }
292 
293         if (!foundRepository && !settings.isOffline()) {
294             builder.add(CENTRAL_REPO);
295         }
296 
297         return Collections.unmodifiableList(builder);
298     }
299 }