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