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