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.Arrays; 025import java.util.EnumSet; 026import java.util.Map; 027import java.util.Objects; 028import java.util.function.BiConsumer; 029import java.util.stream.Stream; 030import org.apache.maven.settings.Profile; 031import org.apache.maven.settings.Settings; 032import org.apache.maven.settings.building.DefaultSettingsBuilderFactory; 033import org.apache.maven.settings.building.DefaultSettingsBuildingRequest; 034import org.apache.maven.settings.building.SettingsBuilder; 035import org.apache.maven.settings.building.SettingsBuildingException; 036import org.apache.maven.settings.building.SettingsBuildingRequest; 037import org.apache.maven.settings.building.SettingsBuildingResult; 038import org.eclipse.aether.RepositorySystem; 039import org.eclipse.aether.RepositorySystemSession; 040import org.eclipse.aether.impl.RepositoryConnectorProvider; 041import org.eclipse.aether.internal.impl.Maven2RepositoryLayoutFactory; 042import org.eclipse.aether.repository.RemoteRepository; 043import org.eclipse.aether.repository.RepositoryPolicy; 044import org.eclipse.aether.spi.connector.layout.RepositoryLayoutFactory; 045import org.eclipse.aether.supplier.RepositorySystemSupplier; 046import org.eclipse.aether.supplier.SessionBuilderSupplier; 047import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector; 048import org.eclipse.aether.util.graph.version.ContextualSnapshotVersionFilter; 049import org.jdrupes.builder.api.BuildException; 050 051/// Manages a global instance of [RepositorySystem] and 052/// [RepositorySystemSession] and provides lists of [RemoteRepository]s 053/// from profiles in `settings.xml`. 054/// 055public final class MavenContext { 056 057 @SuppressWarnings("PMD.AvoidUsingVolatile") 058 private static volatile SessionData theSession; 059 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 060 private static final RemoteRepository MAVEN_CENTRAL_REPO 061 = new RemoteRepository.Builder("central", "default", 062 "https://repo.maven.apache.org/maven2") 063 .setReleasePolicy( 064 createDefaultPolicy(MvnVersionType.RELEASE, true)) 065 .setSnapshotPolicy( 066 createDefaultPolicy(MvnVersionType.SNAPSHOT, false)) 067 .build(); 068 private static final RemoteRepository JDBLD_DISTRIBUTION_REPO 069 = new RemoteRepository.Builder("jdbld-distribution", "default", 070 "https://codeberg.org/api/packages/JDrupes/maven") 071 .setReleasePolicy( 072 createDefaultPolicy(MvnVersionType.RELEASE, true)) 073 .setSnapshotPolicy( 074 createDefaultPolicy(MvnVersionType.SNAPSHOT, false)) 075 .build(); 076 077 private MavenContext() { 078 } 079 080 private record SessionData(Settings settings, 081 RepositorySystem repositorySystem, 082 RepositoryConnectorProvider connectorProvider, 083 RepositorySystemSession repositorySession) { 084 } 085 086 /// Returns the singleton, lazily created session data. 087 /// 088 /// @return the session data 089 /// 090 @SuppressWarnings("PMD.AvoidSynchronizedStatement") 091 private static SessionData session() { 092 if (theSession != null) { 093 return theSession; 094 } 095 synchronized (MavenContext.class) { 096 if (theSession != null) { 097 return theSession; 098 } 099 return initSession(); 100 } 101 } 102 103 private static SessionData initSession() { 104 // Settings 105 SettingsBuildingRequest settingsRequest 106 = new DefaultSettingsBuildingRequest().setUserSettingsFile( 107 new File(System.getProperty("user.home"), ".m2/settings.xml")); 108 SettingsBuilder settingsBuilder 109 = new DefaultSettingsBuilderFactory().newInstance(); 110 SettingsBuildingResult settingsResult; 111 try { 112 settingsResult = settingsBuilder.build(settingsRequest); 113 } catch (SettingsBuildingException e) { 114 throw new BuildException().cause(e); 115 } 116 var settings = settingsResult.getEffectiveSettings(); 117 118 // Repository system supplier 119 var supplier = new RepositorySystemSupplier() { 120 @Override 121 protected Map<String, RepositoryLayoutFactory> 122 createRepositoryLayoutFactories() { 123 var factories = super.createRepositoryLayoutFactories(); 124 var maven2 = factories.get(Maven2RepositoryLayoutFactory.NAME); 125 factories.put(Maven2RepositoryLayoutFactory.NAME, 126 new NoMetadataChecksumLayoutFactory(maven2)); 127 return factories; 128 } 129 }; 130 // Connector provider 131 var connectorProvider = supplier.getRepositoryConnectorProvider(); 132 // Repository system 133 @SuppressWarnings("PMD.CloseResource") 134 var repoSystem = supplier.get(); 135 // Repository system session 136 String localRepoPath = settings.getLocalRepository() != null 137 ? settings.getLocalRepository() 138 : System.getProperty("user.home") + "/.m2/repository"; 139 @SuppressWarnings("PMD.CloseResource") 140 var session = new SessionBuilderSupplier(repoSystem).get() 141 .withLocalRepositoryBaseDirectories(Path.of(localRepoPath)) 142 .setVersionFilter(new ContextualSnapshotVersionFilter()) 143 .setConfigProperty( 144 ConfigurableVersionSelector.CONFIG_PROP_SELECTION_STRATEGY, 145 ConfigurableVersionSelector.HIGHEST_SELECTION_STRATEGY) 146 .build(); 147 148 // Combine 149 theSession = new SessionData(settings, repoSystem, connectorProvider, 150 session); 151 return theSession; 152 } 153 154 /// Repository system. 155 /// 156 /// @return the repository system 157 /// 158 public static RepositorySystem repositorySystem() { 159 return session().repositorySystem(); 160 } 161 162 /// Connector provider. 163 /// 164 /// @return the repository connector provider 165 /// 166 public static RepositoryConnectorProvider repositoryConnectorProvider() { 167 return session().connectorProvider(); 168 } 169 170 /// Repository session. 171 /// 172 /// @return the repository system session 173 /// 174 public static RepositorySystemSession repositorySession() { 175 return session().repositorySession(); 176 } 177 178 /// Looks up the credentials for the specified server in `settings.xml`. 179 /// Invokes the consumer with the username and password if found. 180 /// 181 /// @param serverId the server id 182 /// @param consumer the consumer 183 /// @return true, if found 184 /// 185 public static boolean lookupCredentials(String serverId, 186 BiConsumer<String, String> consumer) { 187 return session().settings().getServers().stream() 188 .filter(s -> serverId.equals(s.getId())).findFirst().map(s -> { 189 consumer.accept(s.getUsername(), s.getPassword()); 190 return true; 191 }).orElse(false); 192 } 193 194 /// Returns the [RemoteRepository] for Maven Central. 195 /// 196 /// @return the remote repository 197 /// 198 public static RemoteRepository mavenCentral() { 199 return MAVEN_CENTRAL_REPO; 200 } 201 202 /// Returns the [RemoteRepository] for the JDrupes Builder distribution 203 /// repository. 204 /// 205 /// @return the remote repository 206 /// 207 public static RemoteRepository jdbldDistribution() { 208 return JDBLD_DISTRIBUTION_REPO; 209 } 210 211 /// Return the [RemoteRepository]s from the specified profile. 212 /// 213 /// @param profileId the profile id 214 /// @return the repositories 215 /// 216 public static Stream<RemoteRepository> repositories(String profileId) { 217 Objects.requireNonNull(profileId); 218 return repositories(session().settings(), profileId); 219 } 220 221 /// Return the [RemoteRepository]s from the specified settings and profile. 222 /// 223 /// @param settings the settings 224 /// @param profileId the profile id 225 /// @return the repositories 226 /// 227 public static Stream<RemoteRepository> repositories(Settings settings, 228 String profileId) { 229 Objects.requireNonNull(profileId); 230 if (!settings.getActiveProfiles().contains(profileId)) { 231 return Stream.empty(); 232 } 233 Map<String, Profile> profiles = settings.getProfilesAsMap(); 234 Profile profile = profiles.get(profileId); 235 if (profile == null) { 236 return Stream.empty(); 237 } 238 return profile.getRepositories().stream().map(repo -> { 239 var builder = new RemoteRepository.Builder(repo.getId(), 240 "default", repo.getUrl()) 241 .setReleasePolicy(createPolicy(MvnVersionType.RELEASE, 242 repo.getReleases())) 243 .setSnapshotPolicy(createPolicy(MvnVersionType.SNAPSHOT, 244 repo.getSnapshots())); 245 return builder.build(); 246 }); 247 } 248 249 /// Creates a [RemoteRepository] from the specified id and [URI] 250 /// that supports lookup for the specified version types. 251 /// 252 /// The repository uses default policies as returned by 253 /// [createDefaultPolicy(MvnVersionType, boolean)]. 254 /// 255 /// @param id the id 256 /// @param uri the uri 257 /// @param supported the supported 258 /// @return the remote repository 259 /// 260 public static RemoteRepository createRepository( 261 String id, URI uri, MvnVersionType... supported) { 262 var types = EnumSet.copyOf(Arrays.asList(supported)); 263 var builder = new RemoteRepository.Builder( 264 id, "default", uri.toString()) 265 .setReleasePolicy(createDefaultPolicy(MvnVersionType.RELEASE, 266 types.contains(MvnVersionType.RELEASE))) 267 .setSnapshotPolicy(createDefaultPolicy(MvnVersionType.SNAPSHOT, 268 types.contains(MvnVersionType.SNAPSHOT))); 269 return builder.build(); 270 } 271 272 /// Creates a policy for the specified type with reasonable defaults. 273 /// See [createPolicy(MvnVersionType, boolean, String, String)]. 274 /// 275 /// @param type the type 276 /// @param enabled the enabled 277 /// @return the repository policy 278 /// 279 public static RepositoryPolicy createDefaultPolicy( 280 MvnVersionType type, boolean enabled) { 281 return createPolicy(type, enabled, null, null); 282 } 283 284 /// Creates a policy from settings data. Fills in reasonable defaults if 285 /// necessary. 286 /// 287 /// @param type the type 288 /// @param policy the policy data from settings 289 /// @return the repository policy 290 /// 291 public static RepositoryPolicy createPolicy(MvnVersionType type, 292 org.apache.maven.settings.RepositoryPolicy policy) { 293 if (policy == null) { 294 return createPolicy(type, false, null, null); 295 } 296 return createPolicy(type, policy.isEnabled(), 297 policy.getUpdatePolicy(), policy.getChecksumPolicy()); 298 } 299 300 /// Creates a policy from settings data with the given details. 301 /// Fills in reasonable defaults for `null` values. 302 /// 303 /// @param type the type 304 /// @param enabled the enabled 305 /// @param updatePolicy the update policy. Defaults to 306 /// "daily" for snapshots and "always" for releases 307 /// @param checksumPolicy the checksum policy. Defaults to "warn" 308 /// @return the repository policy 309 /// 310 public static RepositoryPolicy createPolicy(MvnVersionType type, 311 boolean enabled, String updatePolicy, String checksumPolicy) { 312 if (updatePolicy == null) { 313 updatePolicy = type == MvnVersionType.SNAPSHOT 314 ? RepositoryPolicy.UPDATE_POLICY_ALWAYS 315 : RepositoryPolicy.UPDATE_POLICY_DAILY; 316 } 317 if (checksumPolicy == null) { 318 checksumPolicy = RepositoryPolicy.CHECKSUM_POLICY_WARN; 319 } 320 return new RepositoryPolicy(enabled, updatePolicy, checksumPolicy); 321 } 322}