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      private static final RemoteRepository CENTRAL_REPO = new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build();
75  
76      private static final List<RemoteRepository> knownRemoteRepos = List.of(
77              CENTRAL_REPO,
78              new RemoteRepository.Builder("snapshots", "default", "https://central.sonatype.com/repository/maven-snapshots/").build()
79      );
80  
81      private static final String USER_HOME = System.getProperty("user.home");
82      private static final File USER_MAVEN_HOME = new File(USER_HOME, ".m2");
83      private static final String ENV_M2_HOME = System.getenv("M2_HOME");
84  
85      private static final File DEFAULT_USER_SETTINGS_FILE = new File(USER_MAVEN_HOME, "settings.xml");
86      private static final File DEFAULT_USER_REPOSITORY = new File(USER_MAVEN_HOME, "repository");
87      private static final File DEFAULT_GLOBAL_SETTINGS_FILE =
88              new File(System.getProperty("maven.home", Objects.requireNonNullElse(ENV_M2_HOME, "")), "conf/settings.xml");
89  
90      private final RepositorySystem repositorySystem;
91      private final RepositorySystemSession mavenSession;
92      private final List<RemoteRepository> remoteRepositories;
93  
94      private final String extension;
95  
96      /**
97       * Creates a new artifact loader for 'jar' artifacts.
98       */
99      public MavenArtifactLoader() {
100         this("jar");
101     }
102 
103     /**
104      * Creates a new artifact loader for artifacts.
105      *
106      * @param extension The artifact extension. Must not be null.
107      */
108     public MavenArtifactLoader(String extension) {
109         this(extension, null);
110     }
111 
112     static MavenArtifactLoader forTesting() {
113         return new MavenArtifactLoader("jar", knownRemoteRepos);
114     }
115 
116     private MavenArtifactLoader(String extension, List<RemoteRepository> remoteRepositoriesForTesting) {
117         this.extension = requireNonNull(extension, "extension is null");
118 
119         @SuppressWarnings("deprecation")
120         ServiceLocator serviceLocator = createServiceLocator();
121         this.repositorySystem = serviceLocator.getService(RepositorySystem.class);
122 
123         try {
124             Settings settings = createSettings();
125             File localRepositoryLocation = settings.getLocalRepository() != null ? new File(settings.getLocalRepository()) : DEFAULT_USER_REPOSITORY;
126             LocalRepository localRepository = new LocalRepository(localRepositoryLocation);
127 
128             if (remoteRepositoriesForTesting != null) {
129                 this.remoteRepositories = remoteRepositoriesForTesting;
130             } else {
131                 this.remoteRepositories = extractRemoteRepositories(settings);
132             }
133 
134             DefaultRepositorySystemSession mavenSession = MavenRepositorySystemUtils.newSession();
135 
136             this.mavenSession = mavenSession.setLocalRepositoryManager(repositorySystem.newLocalRepositoryManager(mavenSession, localRepository));
137 
138         } catch (SettingsBuildingException e) {
139             throw new IllegalStateException("Could not load maven settings:", e);
140         }
141     }
142 
143     /**
144      * Create a new version match builder to retrieve an artifact.
145      *
146      * @param groupId    The Apache Maven Group Id. Must not be null.
147      * @param artifactId The Apache Maven Artifact Id. Must not be null.
148      * @return A {@link MavenVersionMatchBuilder} instance
149      */
150     public MavenVersionMatchBuilder builder(String groupId, String artifactId) {
151         requireNonNull(groupId, "groupId is null");
152         requireNonNull(artifactId, "artifactId is null");
153 
154         return new MavenVersionMatchBuilder(this, groupId, artifactId);
155     }
156 
157     /**
158      * Download an artifact file from the Maven repository system.
159      *
160      * @param groupId    The Apache Maven Group Id. Must not be null.
161      * @param artifactId The Apache Maven Artifact Id. Must not be null.
162      * @param version    The Apache Maven Artifact version. Must not be null.
163      * @return A file representing a successfully downloaded artifact.
164      * @throws IOException If the artifact could not be found or an IO problem happened while locating or downloading the artifact.
165      */
166     public File getArtifactFile(String groupId, String artifactId, String version) throws IOException {
167         requireNonNull(groupId, "groupId is null");
168         requireNonNull(artifactId, "artifactId is null");
169         requireNonNull(version, "version is null");
170 
171         ArtifactRequest artifactRequest = new ArtifactRequest();
172         artifactRequest.setArtifact(new DefaultArtifact(groupId, artifactId, extension, version));
173         artifactRequest.setRepositories(this.remoteRepositories);
174         try {
175             ArtifactResult artifactResult = this.repositorySystem.resolveArtifact(mavenSession, artifactRequest);
176             Artifact artifact = artifactResult.getArtifact();
177             return artifact.getFile();
178         } catch (RepositoryException e) {
179             throw new IOException(e);
180         }
181     }
182 
183     /**
184      * Find a matching artifact version from a partially defined artifact version.
185      * <p>
186      * Any located artifact in the repository system is compared to the version given.
187      * <br>
188      * Using this method is equivalent to calling
189      * <pre>
190      *  builder(groupId, artifactId)
191      *                 .partialMatch(version)
192      *                 .extension(extension)
193      *                 .includeSnapshots(true)
194      *                 .findBestMatch();
195      * </pre>
196      *
197      * but will throw an IOException if no version could be found.
198      *
199      * @param groupId    The Apache Maven Group Id. Must not be null.
200      * @param artifactId The Apache Maven Artifact Id. Must not be null.
201      * @param version    A partial version string. Must not be null. An empty string matches any version.
202      * @return The latest version that matches the partial version string. It either starts with the partial version string given (an empty version string
203      * matches any version) or is exactly the provided version.
204      * @throws IOException If an IO problem happened during artifact download or no versions were found during resolution.
205      */
206     public String findLatestVersion(String groupId, String artifactId, String version) throws IOException {
207         requireNonNull(groupId, "groupId is null");
208         requireNonNull(artifactId, "artifactId is null");
209         requireNonNull(version, "version is null");
210         return builder(groupId, artifactId)
211                 .partialMatch(version)
212                 .extension(extension)
213                 .includeSnapshots(true)
214                 .findBestMatch()
215                 .orElseThrow(() -> new IOException(format("No suitable candidate for %s:%s:%s found!", groupId, artifactId, version)));
216     }
217 
218     SortedSet<Version> findAllVersions(MavenVersionMatchBuilder builder) throws IOException {
219 
220         Artifact artifact = new DefaultArtifact(builder.groupId(), builder.artifactId(), builder.extension(), "[0,)");
221 
222         VersionRangeRequest rangeRequest = new VersionRangeRequest();
223         rangeRequest.setArtifact(artifact);
224         rangeRequest.setRepositories(this.remoteRepositories);
225 
226         try {
227             VersionRangeResult rangeResult = this.repositorySystem.resolveVersionRange(mavenSession, rangeRequest);
228             SortedSet<Version> resultBuilder = new TreeSet<>();
229             List<Version> artifactVersions = rangeResult.getVersions();
230             VersionStrategy versionStrategy = builder.versionStrategy();
231             if (artifactVersions != null) {
232                 for (Version artifactVersion : artifactVersions) {
233                     boolean isSnapshot = artifactVersion.toString().endsWith("-SNAPSHOT");
234                     boolean match = versionStrategy.matchVersion(artifactVersion);
235 
236                     // remove match if snapshots are not requested but the version is a snapshot
237                     if (isSnapshot) {
238                         match &= builder.includeSnapshots();
239                     }
240 
241                     if (match) {
242                         resultBuilder.add(artifactVersion);
243                     }
244                 }
245             }
246             return Collections.unmodifiableSortedSet(resultBuilder);
247         } catch (VersionRangeResolutionException e) {
248             throw new IOException(format("Could not resolve version range: %s", rangeRequest), e);
249         }
250     }
251 
252 
253     @SuppressWarnings("deprecation")
254     private static ServiceLocator createServiceLocator() {
255         DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
256 
257         locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
258         locator.addService(TransporterFactory.class, FileTransporterFactory.class);
259         locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
260 
261         locator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() {
262             @Override
263             public void serviceCreationFailed(Class<?> type, Class<?> impl, Throwable e) {
264                 LOG.error(format("Could not create instance of %s (implementation %s): ", type.getSimpleName(), impl.getSimpleName()), e);
265             }
266         });
267 
268         return locator;
269     }
270 
271     private static Settings createSettings() throws SettingsBuildingException {
272         SettingsBuildingRequest settingsBuildingRequest = new DefaultSettingsBuildingRequest()
273                 .setSystemProperties(System.getProperties())
274                 .setUserSettingsFile(DEFAULT_USER_SETTINGS_FILE)
275                 .setGlobalSettingsFile(DEFAULT_GLOBAL_SETTINGS_FILE);
276 
277         DefaultSettingsBuilderFactory settingBuilderFactory = new DefaultSettingsBuilderFactory();
278         DefaultSettingsBuilder settingsBuilder = settingBuilderFactory.newInstance();
279         SettingsBuildingResult settingsBuildingResult = settingsBuilder.build(settingsBuildingRequest);
280 
281         return settingsBuildingResult.getEffectiveSettings();
282     }
283 
284     private static List<RemoteRepository> extractRemoteRepositories(Settings settings) {
285         Map<String, Profile> profiles = settings.getProfilesAsMap();
286         List<RemoteRepository> builder = new ArrayList<>();
287 
288         boolean foundRepository = false;
289         for (String profileName : settings.getActiveProfiles()) {
290             Profile profile = profiles.get(profileName);
291             if (profile != null) {
292                 List<Repository> repositories = profile.getRepositories();
293                 if (repositories != null) {
294                     for (Repository repo : repositories) {
295                         builder.add(new RemoteRepository.Builder(repo.getId(), "default", repo.getUrl()).build());
296                         foundRepository = true;
297                     }
298                 }
299             }
300         }
301 
302         if (!foundRepository && !settings.isOffline()) {
303             builder.add(CENTRAL_REPO);
304         }
305 
306         return Collections.unmodifiableList(builder);
307     }
308 }