001/* 002 * JDrupes Builder 003 * Copyright (C) 2025 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.Map; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.stream.Stream; 032import org.eclipse.aether.RepositorySystem; 033import org.eclipse.aether.RepositorySystemSession; 034import org.eclipse.aether.artifact.Artifact; 035import org.eclipse.aether.artifact.DefaultArtifact; 036import org.eclipse.aether.collection.CollectRequest; 037import org.eclipse.aether.graph.Dependency; 038import org.eclipse.aether.graph.DependencyNode; 039import org.eclipse.aether.repository.RemoteRepository; 040import org.eclipse.aether.repository.RepositoryPolicy; 041import org.eclipse.aether.resolution.ArtifactRequest; 042import org.eclipse.aether.resolution.ArtifactResolutionException; 043import org.eclipse.aether.resolution.DependencyRequest; 044import org.eclipse.aether.resolution.DependencyResolutionException; 045import org.eclipse.aether.util.artifact.SubArtifact; 046import org.eclipse.aether.util.graph.visitor.PreorderNodeListGenerator; 047import org.jdrupes.builder.api.BuildException; 048import org.jdrupes.builder.api.Resource; 049import org.jdrupes.builder.api.ResourceFactory; 050import org.jdrupes.builder.api.ResourceProvider; 051import org.jdrupes.builder.api.ResourceRequest; 052import org.jdrupes.builder.api.ResourceType; 053import static org.jdrupes.builder.api.ResourceType.*; 054import org.jdrupes.builder.api.Resources; 055import org.jdrupes.builder.core.AbstractProvider; 056import org.jdrupes.builder.java.CompilationResources; 057import org.jdrupes.builder.java.LibraryJarFile; 058import org.jdrupes.builder.mvnrepo.MvnRepoDependency.Scope; 059import static org.jdrupes.builder.mvnrepo.MvnRepoTypes.*; 060 061/// Depending on the request, this provider provides two types of resources. 062/// 063/// 1. The artifacts to be resolved as 064/// `CompilationResources<MavenRepoDependencies>`. The artifacts 065/// to be resolved are those added with [resolve]. 066/// 067/// 2. The `CompilationResources<LibraryJarFile>` 068/// or `RuntimeResources<LibraryJarFile>` (depending on the 069/// request) that result from resolving the artifacts to be resolved. 070/// The resources returned implement the additional marker interface 071/// `MvnRepoJarFile`. 072/// 073public class MvnRepoLookup extends AbstractProvider 074 implements ResourceProvider { 075 076 private final Map<ResourceType<? extends Resources<?>>, 077 List<String>> coordinates = new ConcurrentHashMap<>(); 078 private boolean downloadSources = true; 079 private boolean downloadJavadoc = true; 080 private URI snapshotUri; 081 private static Context rootContextInstance; 082 083 /// Instantiates a new mvn repo lookup. 084 /// 085 @SuppressWarnings("PMD.UnnecessaryConstructor") 086 public MvnRepoLookup() { 087 // Make javadoc happy. 088 } 089 090 /// Lazily creates the root context. 091 /// @return the context 092 /// 093 /* default */ static Context rootContext() { 094 if (rootContextInstance != null) { 095 return rootContextInstance; 096 } 097 ContextOverrides overrides = ContextOverrides.create() 098 .withUserSettings(true).build(); 099 Runtime runtime = Runtimes.INSTANCE.getRuntime(); 100 rootContextInstance = runtime.create(overrides); 101 return rootContextInstance; 102 } 103 104 /// Sets the Maven snapshot repository URI. 105 /// 106 /// @param uri the snapshot repository URI 107 /// @return the mvn repo lookup 108 /// 109 public MvnRepoLookup snapshotRepository(URI uri) { 110 this.snapshotUri = uri; 111 return this; 112 } 113 114 /// Returns the snapshot repository. Defaults to 115 /// `https://central.sonatype.com/repository/maven-snapshots/`. 116 /// 117 /// @return the uri 118 /// 119 public URI snapshotRepository() { 120 return snapshotUri; 121 } 122 123 /// Add artifacts, specified by their coordinates 124 /// (`groupId:artifactId:version`) with the given scope. 125 /// 126 /// @param scope the scope 127 /// @param coordinates the coordinates 128 /// @return the mvn repo lookup 129 /// 130 public MvnRepoLookup resolve(Scope scope, String... coordinates) { 131 this.coordinates 132 .computeIfAbsent(scope == Scope.Compile ? MvnRepoCompilationDepsType 133 : MvnRepoRuntimeDepsType, _ -> new ArrayList<>()) 134 .addAll(Arrays.asList(coordinates)); 135 return this; 136 } 137 138 /// Add artifacts, specified by their coordinates 139 /// (`groupId:artifactId:version`) as compilation resources. 140 /// 141 /// @param coordinates the coordinates 142 /// @return the mvn repo lookup 143 /// 144 public MvnRepoLookup resolve(String... coordinates) { 145 return resolve(Scope.Compile, coordinates); 146 } 147 148 /// Whether to also download the sources. Defaults to `true`. 149 /// 150 /// @param enable the enable 151 /// @return the mvn repo lookup 152 /// 153 public MvnRepoLookup downloadSources(boolean enable) { 154 this.downloadSources = enable; 155 return this; 156 } 157 158 /// Whether to also download the javadoc. Defaults to `true`. 159 /// 160 /// @param enable the enable 161 /// @return the mvn repo lookup 162 /// 163 public MvnRepoLookup downloadJavadoc(boolean enable) { 164 this.downloadJavadoc = enable; 165 return this; 166 } 167 168 /// Provide. 169 /// 170 /// @param <T> the generic type 171 /// @param requested the requested resources 172 /// @return the stream 173 /// 174 @Override 175 protected <T extends Resource> Stream<T> 176 doProvide(ResourceRequest<T> requested) { 177 if (requested.accepts(MvnRepoCompilationDepsType)) { 178 @SuppressWarnings("unchecked") 179 var result = (Stream<T>) coordinates.entrySet().stream() 180 .filter(e -> requested.accepts(e.getKey())) 181 .map(e -> e.getValue().stream().map(c -> ResourceFactory 182 .create(MvnRepoDependencyType, null, c, 183 e.getKey().equals(MvnRepoCompilationDepsType) 184 ? Scope.Compile 185 : Scope.Runtime))) 186 .flatMap(d -> d); 187 return result; 188 } 189 if (requested.accepts( 190 new ResourceType<CompilationResources<LibraryJarFile>>() {})) { 191 return provideJars(requested); 192 } 193 return Stream.empty(); 194 } 195 196 private <T extends Resource> Stream<T> 197 provideJars(ResourceRequest<T> requested) { 198 // Base collect request, optionally with snapshots 199 CollectRequest collectRequest = new CollectRequest().setRepositories( 200 new ArrayList<>(rootContext().remoteRepositories())); 201 if (snapshotUri != null) { 202 addSnapshotRepository(collectRequest); 203 } 204 205 // Retrieve coordinates and add to collect request 206 var asDepsType = resourceType( 207 requested.type().rawType(), MvnRepoDependency.class); 208 coordinates.entrySet().stream() 209 .filter(e -> asDepsType.isAssignableFrom(e.getKey())) 210 .forEach(e -> e.getValue().stream().forEach(c -> collectRequest 211 .addDependency(new Dependency(new DefaultArtifact(c), 212 e.getKey().equals(MvnRepoCompilationDepsType) ? "compile" 213 : "runtime")))); 214 215 DependencyRequest dependencyRequest 216 = new DependencyRequest(collectRequest, null); 217 DependencyNode rootNode; 218 try { 219 var repoSystem = rootContext().repositorySystem(); 220 var repoSession = rootContext().repositorySystemSession(); 221 rootNode = repoSystem.resolveDependencies(repoSession, 222 dependencyRequest).getRoot(); 223// For maven 2.x libraries: 224// List<DependencyNode> dependencyNodes = new ArrayList<>(); 225// rootNode.accept(new PreorderDependencyNodeConsumerVisitor( 226// dependencyNodes::add)); 227 PreorderNodeListGenerator nlg = new PreorderNodeListGenerator(); 228 rootNode.accept(nlg); 229 List<DependencyNode> dependencyNodes = nlg.getNodes(); 230 @SuppressWarnings("unchecked") 231 var result = (Stream<T>) dependencyNodes.stream() 232 .filter(d -> d.getArtifact() != null) 233 .map(DependencyNode::getArtifact) 234 .map(a -> { 235 if (downloadSources) { 236 downloadSourceJar(repoSystem, repoSession, a); 237 } 238 if (downloadJavadoc) { 239 downloadJavadocJar(repoSystem, repoSession, a); 240 } 241 return a; 242 }).map(a -> a.getFile().toPath()) 243 .map(p -> ResourceFactory.create(MvnRepoLibraryJarFileType, p)); 244 return result; 245 } catch (DependencyResolutionException e) { 246 throw new BuildException( 247 "Cannot resolve: " + e.getMessage(), e); 248 } 249 } 250 251 private void addSnapshotRepository(CollectRequest collectRequest) { 252 RemoteRepository snapshotsRepo = new RemoteRepository.Builder( 253 "snapshots", "default", snapshotUri.toString()) 254 .setSnapshotPolicy(new RepositoryPolicy( 255 true, // enable snapshots 256 RepositoryPolicy.UPDATE_POLICY_ALWAYS, 257 RepositoryPolicy.CHECKSUM_POLICY_WARN)) 258 .setReleasePolicy(new RepositoryPolicy( 259 false, 260 RepositoryPolicy.UPDATE_POLICY_NEVER, 261 RepositoryPolicy.CHECKSUM_POLICY_IGNORE)) 262 .build(); 263 collectRequest.addRepository(snapshotsRepo); 264 } 265 266 private void downloadSourceJar(RepositorySystem repoSystem, 267 RepositorySystemSession repoSession, Artifact jarArtifact) { 268 Artifact sourcesArtifact 269 = new SubArtifact(jarArtifact, "sources", "jar"); 270 ArtifactRequest sourcesRequest = new ArtifactRequest(); 271 sourcesRequest.setArtifact(sourcesArtifact); 272 sourcesRequest.setRepositories(rootContext().remoteRepositories()); 273 try { 274 repoSystem.resolveArtifact(repoSession, sourcesRequest); 275 } catch (ArtifactResolutionException e) { // NOPMD 276 // Ignore, sources are optional 277 } 278 } 279 280 private void downloadJavadocJar(RepositorySystem repoSystem, 281 RepositorySystemSession repoSession, Artifact jarArtifact) { 282 Artifact javadocArtifact 283 = new SubArtifact(jarArtifact, "javadoc", "jar"); 284 ArtifactRequest sourcesRequest = new ArtifactRequest(); 285 sourcesRequest.setArtifact(javadocArtifact); 286 sourcesRequest.setRepositories(rootContext().remoteRepositories()); 287 try { 288 repoSystem.resolveArtifact(repoSession, sourcesRequest); 289 } catch (ArtifactResolutionException e) { // NOPMD 290 // Ignore, javadoc is optional 291 } 292 } 293}