001/* 002 * JDrupes Builder 003 * Copyright (C) 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 java.io.File; 022import java.net.URI; 023import java.nio.file.Path; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Collections; 027import java.util.EnumSet; 028import java.util.HashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.Set; 032import java.util.function.BiConsumer; 033import org.apache.maven.settings.Profile; 034import org.apache.maven.settings.Repository; 035import org.apache.maven.settings.Settings; 036import org.apache.maven.settings.building.DefaultSettingsBuilderFactory; 037import org.apache.maven.settings.building.DefaultSettingsBuildingRequest; 038import org.apache.maven.settings.building.SettingsBuilder; 039import org.apache.maven.settings.building.SettingsBuildingException; 040import org.apache.maven.settings.building.SettingsBuildingRequest; 041import org.apache.maven.settings.building.SettingsBuildingResult; 042import org.eclipse.aether.RepositorySystem; 043import org.eclipse.aether.RepositorySystemSession; 044import org.eclipse.aether.repository.RemoteRepository; 045import org.eclipse.aether.repository.RepositoryPolicy; 046import org.eclipse.aether.supplier.RepositorySystemSupplier; 047import org.eclipse.aether.supplier.SessionBuilderSupplier; 048import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector; 049import org.jdrupes.builder.api.BuildException; 050 051/// Manages a global instance of [RepositorySystem] and 052/// [RepositorySystemSession] and a list of [RemoteRepository]s for 053/// use by all instances of [MvnRepoLookup] and others. 054/// 055/// The remote repositories are evaluated by looking up the repositories 056/// from the profiles in `settings.xml` selected with [useProfiles] and the 057/// repositories added with [addRepository]. 058/// 059public final class MavenContext { 060 061 @SuppressWarnings("PMD.AvoidUsingVolatile") 062 private static volatile SessionData theSession; 063 private static final RemoteRepository MAVEN_CENTRAL_REPO 064 = new RemoteRepository.Builder("central", "default", 065 "https://repo.maven.apache.org/maven2") 066 .setSnapshotPolicy(new RepositoryPolicy(false, 067 RepositoryPolicy.UPDATE_POLICY_NEVER, 068 RepositoryPolicy.CHECKSUM_POLICY_IGNORE)) 069 .build(); 070 @SuppressWarnings("PMD.FieldNamingConventions") 071 private static final Set<String> useProfiles 072 = Collections.synchronizedSet(new HashSet<>()); 073 @SuppressWarnings("PMD.AvoidUsingVolatile") 074 private static volatile List<RemoteRepository> settingsRepos; 075 @SuppressWarnings("PMD.FieldNamingConventions") 076 private static final List<RemoteRepository> addedRepos 077 = Collections.synchronizedList(new ArrayList<>()); 078 079 private MavenContext() { 080 } 081 082 private record SessionData(Settings settings, 083 RepositorySystem repositorySystem, 084 RepositorySystemSession repositorySession) { 085 } 086 087 /// Returns the singleton, lazily created session data. 088 /// 089 /// @return the session data 090 /// 091 @SuppressWarnings("PMD.AvoidSynchronizedStatement") 092 private static SessionData session() { 093 if (theSession != null) { 094 return theSession; 095 } 096 synchronized (MavenContext.class) { 097 if (theSession != null) { 098 return theSession; 099 } 100 return initSession(); 101 } 102 } 103 104 private static SessionData initSession() { 105 // Settings 106 SettingsBuildingRequest settingsRequest 107 = new DefaultSettingsBuildingRequest().setUserSettingsFile( 108 new File(System.getProperty("user.home"), ".m2/settings.xml")); 109 SettingsBuilder settingsBuilder 110 = new DefaultSettingsBuilderFactory().newInstance(); 111 SettingsBuildingResult settingsResult; 112 try { 113 settingsResult = settingsBuilder.build(settingsRequest); 114 } catch (SettingsBuildingException e) { 115 throw new BuildException().cause(e); 116 } 117 var settings = settingsResult.getEffectiveSettings(); 118 119 // Repository system 120 @SuppressWarnings("PMD.CloseResource") 121 var repoSystem = new RepositorySystemSupplier().get(); 122 123 // Repository system session 124 String localRepoPath = settings.getLocalRepository() != null 125 ? settings.getLocalRepository() 126 : System.getProperty("user.home") + "/.m2/repository"; 127 @SuppressWarnings("PMD.CloseResource") 128 var session = new SessionBuilderSupplier(repoSystem).get() 129 .withLocalRepositoryBaseDirectories(Path.of(localRepoPath)) 130 .setConfigProperty( 131 ConfigurableVersionSelector.CONFIG_PROP_SELECTION_STRATEGY, 132 ConfigurableVersionSelector.HIGHEST_SELECTION_STRATEGY) 133 .build(); 134 135 // Combine 136 theSession = new SessionData(settings, repoSystem, session); 137 return theSession; 138 } 139 140 /// Repository system. 141 /// 142 /// @return the repository system 143 /// 144 public static RepositorySystem repositorySystem() { 145 return session().repositorySystem(); 146 } 147 148 /// Repository session. 149 /// 150 /// @return the repository system session 151 /// 152 public static RepositorySystemSession repositorySession() { 153 return session().repositorySession(); 154 } 155 156 /// Looks up the credentials for the specified server in `settings.xml`. 157 /// Invokes the consumer with the username and password if found. 158 /// 159 /// @param serverId the server id 160 /// @param consumer the consumer 161 /// @return true, if found 162 /// 163 public static boolean lookupCredentials(String serverId, 164 BiConsumer<String, String> consumer) { 165 return session().settings().getServers().stream() 166 .filter(s -> serverId.equals(s.getId())).findFirst().map(s -> { 167 consumer.accept(s.getUsername(), s.getPassword()); 168 return true; 169 }).orElse(false); 170 } 171 172 /// Include repositories from the specified profiles in `settings.xml` 173 /// in the result of [remoteRepositories]. 174 /// 175 /// @param profiles the profiles 176 /// @return the Maven context 177 /// 178 public static Class<MavenContext> useProfiles(String... profiles) { 179 if (settingsRepos != null) { 180 throw new IllegalStateException( 181 "Repositories are already evauated"); 182 } 183 useProfiles.addAll(Arrays.asList(profiles)); 184 return MavenContext.class; 185 } 186 187 /// Include the given repository in the result of [remoteRepositories]. 188 /// 189 /// @param repository the repository 190 /// @return the Maven context 191 /// 192 public static Class<MavenContext> 193 addRepository(RemoteRepository repository) { 194 addedRepos.add(repository); 195 return MavenContext.class; 196 } 197 198 /// Include the repository created from the given values in the 199 /// result of [remoteRepositories]. 200 /// 201 /// @param id the repository id 202 /// @param uri the repository uri 203 /// @param supported the supported version types 204 /// @return the Maven context 205 /// 206 public static Class<MavenContext> addRepository( 207 String id, URI uri, MvnVersionType... supported) { 208 var types = EnumSet.copyOf(Arrays.asList(supported)); 209 var builder = new RemoteRepository.Builder( 210 id, "default", uri.toString()) 211 .setReleasePolicy(createPolicy(MvnVersionType.RELEASE, 212 types.contains(MvnVersionType.RELEASE), null, null)) 213 .setSnapshotPolicy(createPolicy(MvnVersionType.SNAPSHOT, 214 types.contains(MvnVersionType.SNAPSHOT), null, null)); 215 addRepository(builder.build()); 216 return MavenContext.class; 217 } 218 219 /// Returns the [RemoteRepository] for Maven Central. 220 /// 221 /// @return the remote repository 222 /// 223 public static RemoteRepository mavenCentral() { 224 return MAVEN_CENTRAL_REPO; 225 } 226 227 /* default */ @SuppressWarnings("PMD.AvoidSynchronizedStatement") 228 static List<RemoteRepository> remoteRepositories() { 229 if (settingsRepos == null) { 230 synchronized (MavenContext.class) { 231 if (settingsRepos == null) { 232 settingsRepos = evaluateSettingsRepositories(); 233 } 234 } 235 } 236 var mergedRepos = new ArrayList<>(settingsRepos); 237 mergedRepos.addAll(addedRepos); 238 return mergedRepos; 239 } 240 241 private static List<RemoteRepository> evaluateSettingsRepositories() { 242 // Add repositories from profiles 243 List<RemoteRepository> repos = new ArrayList<>(); 244 var settings = session().settings(); 245 Map<String, Profile> profiles = settings.getProfilesAsMap(); 246 for (String profileId : settings.getActiveProfiles()) { 247 Profile profile = profiles.get(profileId); 248 if (profile == null || !useProfiles.contains(profileId)) { 249 continue; 250 } 251 for (Repository repo : profile.getRepositories()) { 252 @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") 253 var builder = new RemoteRepository.Builder(repo.getId(), 254 "default", repo.getUrl()) 255 .setReleasePolicy(createPolicy(MvnVersionType.RELEASE, 256 repo.getReleases())) 257 .setSnapshotPolicy(createPolicy(MvnVersionType.SNAPSHOT, 258 repo.getSnapshots())); 259 repos.add(builder.build()); 260 } 261 } 262 return repos; 263 } 264 265 /* default */ static RepositoryPolicy createPolicy(MvnVersionType type, 266 org.apache.maven.settings.RepositoryPolicy policy) { 267 if (policy == null) { 268 return createPolicy(type, false, null, null); 269 } 270 return createPolicy(type, policy.isEnabled(), 271 policy.getUpdatePolicy(), policy.getChecksumPolicy()); 272 } 273 274 /* default */ static RepositoryPolicy createPolicy(MvnVersionType type, 275 boolean enabled, String updatePolicy, String checksumPolicy) { 276 if (updatePolicy == null) { 277 updatePolicy = type == MvnVersionType.SNAPSHOT 278 ? RepositoryPolicy.UPDATE_POLICY_ALWAYS 279 : RepositoryPolicy.UPDATE_POLICY_NEVER; 280 } 281 if (checksumPolicy == null) { 282 checksumPolicy = RepositoryPolicy.CHECKSUM_POLICY_WARN; 283 } 284 return new RepositoryPolicy(enabled, updatePolicy, checksumPolicy); 285 } 286 287}