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}