001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package de.softwareforge.testing.maven;
016
017import static java.lang.String.format;
018import static java.util.Objects.requireNonNull;
019
020import java.io.File;
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.List;
025import java.util.Map;
026import java.util.Objects;
027import java.util.SortedSet;
028import java.util.TreeSet;
029
030import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
031import org.apache.maven.settings.Profile;
032import org.apache.maven.settings.Repository;
033import org.apache.maven.settings.Settings;
034import org.apache.maven.settings.building.DefaultSettingsBuilder;
035import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
036import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
037import org.apache.maven.settings.building.SettingsBuildingException;
038import org.apache.maven.settings.building.SettingsBuildingRequest;
039import org.apache.maven.settings.building.SettingsBuildingResult;
040import org.eclipse.aether.DefaultRepositorySystemSession;
041import org.eclipse.aether.RepositoryException;
042import org.eclipse.aether.RepositorySystem;
043import org.eclipse.aether.RepositorySystemSession;
044import org.eclipse.aether.artifact.Artifact;
045import org.eclipse.aether.artifact.DefaultArtifact;
046import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
047import org.eclipse.aether.impl.DefaultServiceLocator;
048import org.eclipse.aether.repository.LocalRepository;
049import org.eclipse.aether.repository.RemoteRepository;
050import org.eclipse.aether.resolution.ArtifactRequest;
051import org.eclipse.aether.resolution.ArtifactResult;
052import org.eclipse.aether.resolution.VersionRangeRequest;
053import org.eclipse.aether.resolution.VersionRangeResolutionException;
054import org.eclipse.aether.resolution.VersionRangeResult;
055import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
056import org.eclipse.aether.spi.connector.transport.TransporterFactory;
057import org.eclipse.aether.spi.locator.ServiceLocator;
058import org.eclipse.aether.transport.file.FileTransporterFactory;
059import org.eclipse.aether.transport.http.HttpTransporterFactory;
060import org.eclipse.aether.version.Version;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063
064/**
065 * A quick and dirty artifact loader. Downloads published artifacts from the Maven repository system.
066 * <p>
067 * The loader respects the local maven settings (repositories, mirrors etc.) if present. If no configuration is found, a hard-coded reference to <a
068 * href="https://repo.maven.apache.org/maven2/">Maven Central</a> is used.
069 */
070public final class MavenArtifactLoader {
071
072    private static final Logger LOG = LoggerFactory.getLogger(MavenArtifactLoader.class);
073
074    static final RemoteRepository CENTRAL_REPO = new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build();
075
076    private static final String USER_HOME = System.getProperty("user.home");
077    private static final File USER_MAVEN_HOME = new File(USER_HOME, ".m2");
078    private static final String ENV_M2_HOME = System.getenv("M2_HOME");
079
080    private static final File DEFAULT_USER_SETTINGS_FILE = new File(USER_MAVEN_HOME, "settings.xml");
081    private static final File DEFAULT_USER_REPOSITORY = new File(USER_MAVEN_HOME, "repository");
082    private static final File DEFAULT_GLOBAL_SETTINGS_FILE =
083            new File(System.getProperty("maven.home", Objects.requireNonNullElse(ENV_M2_HOME, "")), "conf/settings.xml");
084
085    private final RepositorySystem repositorySystem;
086    private final RepositorySystemSession mavenSession;
087    private final List<RemoteRepository> remoteRepositories;
088
089    private final String extension;
090
091    /**
092     * Creates a new artifact loader for 'jar' artifacts.
093     */
094    public MavenArtifactLoader() {
095        this("jar");
096    }
097
098    /**
099     * 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}