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}