001/* 002 * JDrupes Builder 003 * Copyright (C) 2025, 2026 Michael N. Lipp 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <https://www.gnu.org/licenses/>. 017 */ 018 019package org.jdrupes.builder.mvnrepo; 020 021import com.google.common.flogger.FluentLogger; 022import eu.maveniverse.maven.mima.context.Context; 023import eu.maveniverse.maven.mima.context.ContextOverrides; 024import eu.maveniverse.maven.mima.context.Runtime; 025import eu.maveniverse.maven.mima.context.Runtimes; 026import java.net.URI; 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.Collections; 031import java.util.List; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034import org.apache.maven.model.DependencyManagement; 035import org.apache.maven.model.Model; 036import org.apache.maven.model.building.DefaultModelBuilderFactory; 037import org.apache.maven.model.building.DefaultModelBuildingRequest; 038import org.apache.maven.model.building.ModelBuildingException; 039import org.apache.maven.model.building.ModelBuildingRequest; 040import org.eclipse.aether.RepositorySystem; 041import org.eclipse.aether.RepositorySystemSession; 042import org.eclipse.aether.artifact.Artifact; 043import org.eclipse.aether.collection.CollectRequest; 044import org.eclipse.aether.graph.DependencyNode; 045import org.eclipse.aether.repository.RemoteRepository; 046import org.eclipse.aether.repository.RepositoryPolicy; 047import org.eclipse.aether.resolution.ArtifactRequest; 048import org.eclipse.aether.resolution.ArtifactResolutionException; 049import org.eclipse.aether.resolution.DependencyRequest; 050import org.eclipse.aether.resolution.DependencyResolutionException; 051import org.eclipse.aether.util.artifact.SubArtifact; 052import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator; 053import org.jdrupes.builder.api.BuildException; 054import org.jdrupes.builder.api.Resource; 055import org.jdrupes.builder.api.ResourceFactory; 056import org.jdrupes.builder.api.ResourceRequest; 057import org.jdrupes.builder.core.AbstractProvider; 058import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*; 059 060/// Depending on the request, this provider provides two types of resources. 061/// 062/// 1. The artifacts to be resolved as resources of type [MvnRepoDependency]. 063/// The artifacts to be resolved are those added with [resolve]. 064/// Note that the result also includes the [MvnRepoBom]s. 065/// 066/// 2. The resources of type [MvnRepoLibraryJarFile] that result from 067/// resolving the artifacts to be resolved. 068/// 069@SuppressWarnings("PMD.CouplingBetweenObjects") 070public class MvnRepoLookup extends AbstractProvider { 071 072 private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 073 private static Context rootContextInstance; 074 private final List<String> coordinates = new ArrayList<>(); 075 private final List<String> boms = new ArrayList<>(); 076 private boolean downloadSources = true; 077 private boolean downloadJavadoc = true; 078 private URI snapshotUri; 079 private boolean probeMode; 080 081 /// Initializes a new Maven repository lookup. 082 /// 083 public MvnRepoLookup() { 084 // Make javadoc happy. 085 } 086 087 /// Lazily creates the root context. 088 /// 089 /// @return the context 090 /// 091 /* default */ static Context rootContext() { 092 if (rootContextInstance != null) { 093 return rootContextInstance; 094 } 095 ContextOverrides overrides = ContextOverrides.create() 096 .withUserSettings(true).build(); 097 Runtime runtime = Runtimes.INSTANCE.getRuntime(); 098 rootContextInstance = runtime.create(overrides); 099 logger.atFine().log("Using repositories: %s", 100 rootContextInstance.remoteRepositories().stream() 101 .map(RemoteRepository::getUrl) 102 .collect(Collectors.joining(", "))); 103 return rootContextInstance; 104 } 105 106 /// Sets the Maven snapshot repository URI. 107 /// 108 /// @param uri the snapshot repository URI 109 /// @return the mvn repo lookup 110 /// 111 public MvnRepoLookup snapshotRepository(URI uri) { 112 this.snapshotUri = uri; 113 return this; 114 } 115 116 /// Returns the snapshot repository. Defaults to 117 /// `https://central.sonatype.com/repository/maven-snapshots/`. 118 /// 119 /// @return the snapshot repository 120 /// 121 public URI snapshotRepository() { 122 return snapshotUri; 123 } 124 125 /// Add a bill of materials. The coordinates are resolved as 126 /// a dependency with scope `import` which is added to the 127 /// `dependencyManagement` section. 128 /// 129 /// @param coordinates the coordinates 130 /// @return the mvn repo lookup 131 /// 132 public MvnRepoLookup bom(String... coordinates) { 133 boms.addAll(Arrays.asList(coordinates)); 134 return this; 135 } 136 137 /// Add artifacts, specified by their coordinates 138 /// (`groupId:artifactId:version`) as resources. 139 /// 140 /// @param coordinates the coordinates 141 /// @return the mvn repo lookup 142 /// 143 public MvnRepoLookup resolve(String... coordinates) { 144 this.coordinates.addAll(Arrays.asList(coordinates)); 145 return this; 146 } 147 148 /// Add artifacts. The method handles [MvnRepoBom]s correctly. 149 /// 150 /// @param resources the resources 151 /// @return the mvn repo lookup 152 /// 153 public MvnRepoLookup resolve(Stream<? extends MvnRepoResource> resources) { 154 resources.forEach(r -> { 155 if (r instanceof MvnRepoBom) { 156 bom(r.coordinates()); 157 } else { 158 resolve(r.coordinates()); 159 } 160 }); 161 return this; 162 } 163 164 /// Failing to resolve the dependencies normally results in a 165 /// [BuildException], because the requested artifacts are assumed 166 /// to be required for the build. 167 /// 168 /// By invoking this method the provider enters probe mode 169 /// and returns an empty result stream instead of throwing an 170 /// exception if the resolution fails. 171 /// 172 /// @return the mvn repo lookup 173 /// 174 public MvnRepoLookup probe() { 175 probeMode = true; 176 logger.atFine().log("Probe mode enabled for %s", this); 177 return this; 178 } 179 180 /// Whether to also download the sources. Defaults to `true`. 181 /// 182 /// @param enable the enable 183 /// @return the mvn repo lookup 184 /// 185 public MvnRepoLookup downloadSources(boolean enable) { 186 this.downloadSources = enable; 187 return this; 188 } 189 190 /// Whether to also download the javadoc. Defaults to `true`. 191 /// 192 /// @param enable the enable 193 /// @return the mvn repo lookup 194 /// 195 public MvnRepoLookup downloadJavadoc(boolean enable) { 196 this.downloadJavadoc = enable; 197 return this; 198 } 199 200 /// Provide. 201 /// 202 /// @param <T> the generic type 203 /// @param request the requested resources 204 /// @return the stream 205 /// 206 @Override 207 protected <T extends Resource> Collection<T> 208 doProvide(ResourceRequest<T> request) { 209 if (request.accepts(MvnRepoDependencyType)) { 210 return provideMvnDeps(); 211 } 212 if (!request.accepts(MvnRepoLibraryJarFileType)) { 213 return Collections.emptyList(); 214 } 215 if (!request.isFor(MvnRepoLibraryJarFileType)) { 216 @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" }) 217 var result = (Collection<T>) context() 218 .resources(this, of(MvnRepoLibraryJarFileType)).toList(); 219 return result; 220 } 221 try { 222 return provideJars(); 223 } catch (ModelBuildingException e) { 224 throw new BuildException().from(this).cause(e); 225 } catch (DependencyResolutionException e) { 226 if (probeMode) { 227 return Collections.emptyList(); 228 } 229 throw new BuildException().from(this).cause(e); 230 } 231 } 232 233 private <T extends Resource> Collection<T> provideMvnDeps() { 234 @SuppressWarnings("unchecked") 235 var boms = (Stream<T>) this.boms.stream() 236 .map(MvnRepoBom::of); 237 @SuppressWarnings("unchecked") 238 var deps = (Stream<T>) coordinates.stream() 239 .map(MvnRepoDependency::of); 240 return Stream.concat(boms, deps).toList(); 241 } 242 243 private <T extends Resource> Collection<T> provideJars() 244 throws DependencyResolutionException, ModelBuildingException { 245 var repoSystem = rootContext().repositorySystem(); 246 var repoSession = rootContext().repositorySystemSession(); 247 var repos = new ArrayList<>(rootContext().remoteRepositories()); 248 if (snapshotUri != null) { 249 repos.add(createSnapshotRepository()); 250 } 251 252 // Create an effective model. This make sure that BOMs are 253 // handled correctly. 254 Model model = getEffectiveModel(repoSystem, repoSession, 255 repos); 256 257 // Create collect request using data from the (effective) model 258 CollectRequest collectRequest 259 = new CollectRequest().setRepositories(repos); 260 collectRequest.setManagedDependencies( 261 model.getDependencyManagement().getDependencies().stream() 262 .map(DependencyConverter::convert).toList()); 263 model.getDependencies().stream().map(DependencyConverter::convert) 264 .forEach(collectRequest::addDependency); 265 266 // Resolve dependencies 267 DependencyRequest dependencyRequest 268 = new DependencyRequest(collectRequest, null); 269 DependencyNode rootNode; 270 rootNode = repoSystem.resolveDependencies(repoSession, 271 dependencyRequest).getRoot(); 272// For maven 2.x libraries: 273// List<DependencyNode> dependencyNodes = new ArrayList<>(); 274// rootNode.accept(new PreorderDependencyNodeConsumerVisitor( 275// dependencyNodes::add)); 276 PreorderNodeListGenerator nlg = new PreorderNodeListGenerator(); 277 rootNode.accept(nlg); 278 List<DependencyNode> dependencyNodes = nlg.getNodes(); 279 @SuppressWarnings("unchecked") 280 var result = (Collection<T>) dependencyNodes.stream() 281 .filter(d -> d.getArtifact() != null) 282 .map(DependencyNode::getArtifact) 283 .map(a -> { 284 if (downloadSources) { 285 downloadSourceJar(repoSystem, repoSession, a); 286 } 287 if (downloadJavadoc) { 288 downloadJavadocJar(repoSystem, repoSession, a); 289 } 290 return a; 291 }) 292 .map(a -> ResourceFactory.create(MvnRepoLibraryJarFileType, 293 a.toString(), a.getFile().toPath())) 294 .toList(); 295 return result; 296 } 297 298 private Model getEffectiveModel(RepositorySystem repoSystem, 299 RepositorySystemSession repoSession, List<RemoteRepository> repos) 300 throws ModelBuildingException { 301 // First build raw model 302 Model model = new Model(); 303 model.setModelVersion("4.0.0"); 304 model.setGroupId("model.group"); 305 model.setArtifactId("model.artifact"); 306 model.setVersion("0.0.0"); 307 var depMgmt = new DependencyManagement(); 308 model.setDependencyManagement(depMgmt); 309 boms.stream().forEach(c -> { 310 var mvnResource = MvnRepoDependency.of(c); 311 var dep = DependencyConverter.convert(mvnResource, "import"); 312 dep.setType("pom"); 313 depMgmt.addDependency(dep); 314 }); 315 coordinates.forEach(c -> { 316 var mvnResource = MvnRepoDependency.of(c); 317 model.addDependency( 318 DependencyConverter.convert(mvnResource, "compile")); 319 }); 320 321 // Now build (derive) effective model 322 var buildingRequest = new DefaultModelBuildingRequest() 323 .setRawModel(model).setProcessPlugins(false) 324 .setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL) 325 .setModelResolver( 326 new MvnModelResolver(repoSystem, repoSession, repos)); 327 return new DefaultModelBuilderFactory() 328 .newInstance().build(buildingRequest).getEffectiveModel(); 329 } 330 331 private RemoteRepository createSnapshotRepository() { 332 return new RemoteRepository.Builder( 333 "snapshots", "default", snapshotUri.toString()) 334 .setSnapshotPolicy(new RepositoryPolicy( 335 true, // enable snapshots 336 RepositoryPolicy.UPDATE_POLICY_ALWAYS, 337 RepositoryPolicy.CHECKSUM_POLICY_WARN)) 338 .setReleasePolicy(new RepositoryPolicy( 339 false, 340 RepositoryPolicy.UPDATE_POLICY_NEVER, 341 RepositoryPolicy.CHECKSUM_POLICY_IGNORE)) 342 .build(); 343 } 344 345 private void downloadSourceJar(RepositorySystem repoSystem, 346 RepositorySystemSession repoSession, Artifact jarArtifact) { 347 Artifact sourcesArtifact 348 = new SubArtifact(jarArtifact, "sources", "jar"); 349 ArtifactRequest sourcesRequest = new ArtifactRequest(); 350 sourcesRequest.setArtifact(sourcesArtifact); 351 sourcesRequest.setRepositories(rootContext().remoteRepositories()); 352 try { 353 repoSystem.resolveArtifact(repoSession, sourcesRequest); 354 } catch (ArtifactResolutionException e) { // NOPMD 355 // Ignore, sources are optional 356 } 357 } 358 359 private void downloadJavadocJar(RepositorySystem repoSystem, 360 RepositorySystemSession repoSession, Artifact jarArtifact) { 361 Artifact javadocArtifact 362 = new SubArtifact(jarArtifact, "javadoc", "jar"); 363 ArtifactRequest sourcesRequest = new ArtifactRequest(); 364 sourcesRequest.setArtifact(javadocArtifact); 365 sourcesRequest.setRepositories(rootContext().remoteRepositories()); 366 try { 367 repoSystem.resolveArtifact(repoSession, sourcesRequest); 368 } catch (ArtifactResolutionException e) { // NOPMD 369 // Ignore, javadoc is optional 370 } 371 } 372}