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