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 private static final RemoteRepository CENTRAL_REPO = new RemoteRepository.Builder("central", "default", "https://repo.maven.apache.org/maven2/").build(); 075 076 private static final List<RemoteRepository> knownRemoteRepos = List.of( 077 CENTRAL_REPO, 078 new RemoteRepository.Builder("snapshots", "default", "https://central.sonatype.com/repository/maven-snapshots/").build() 079 ); 080 081 private static final String USER_HOME = System.getProperty("user.home"); 082 private static final File USER_MAVEN_HOME = new File(USER_HOME, ".m2"); 083 private static final String ENV_M2_HOME = System.getenv("M2_HOME"); 084 085 private static final File DEFAULT_USER_SETTINGS_FILE = new File(USER_MAVEN_HOME, "settings.xml"); 086 private static final File DEFAULT_USER_REPOSITORY = new File(USER_MAVEN_HOME, "repository"); 087 private static final File DEFAULT_GLOBAL_SETTINGS_FILE = 088 new File(System.getProperty("maven.home", Objects.requireNonNullElse(ENV_M2_HOME, "")), "conf/settings.xml"); 089 090 private final RepositorySystem repositorySystem; 091 private final RepositorySystemSession mavenSession; 092 private final List<RemoteRepository> remoteRepositories; 093 094 private final String extension; 095 096 /** 097 * Creates a new artifact loader for 'jar' artifacts. 098 */ 099 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}