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.nio.file.Path;
023import java.util.Map;
024import java.util.Objects;
025import java.util.function.BiConsumer;
026import java.util.stream.Stream;
027import org.apache.maven.settings.Profile;
028import org.apache.maven.settings.Settings;
029import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
030import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
031import org.apache.maven.settings.building.SettingsBuilder;
032import org.apache.maven.settings.building.SettingsBuildingException;
033import org.apache.maven.settings.building.SettingsBuildingRequest;
034import org.apache.maven.settings.building.SettingsBuildingResult;
035import org.eclipse.aether.RepositorySystem;
036import org.eclipse.aether.RepositorySystemSession;
037import org.eclipse.aether.repository.RemoteRepository;
038import org.eclipse.aether.repository.RepositoryPolicy;
039import org.eclipse.aether.supplier.RepositorySystemSupplier;
040import org.eclipse.aether.supplier.SessionBuilderSupplier;
041import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector;
042import org.jdrupes.builder.api.BuildException;
043
044/// Manages a global instance of [RepositorySystem] and 
045/// [RepositorySystemSession] and provides lists of [RemoteRepository]s
046/// from profiles in `settings.xml`.
047///
048public final class MavenContext {
049
050    @SuppressWarnings("PMD.AvoidUsingVolatile")
051    private static volatile SessionData theSession;
052    private static final RemoteRepository MAVEN_CENTRAL_REPO
053        = new RemoteRepository.Builder("central", "default",
054            "https://repo.maven.apache.org/maven2")
055                .setReleasePolicy(
056                    createDefaultPolicy(MvnVersionType.RELEASE, true))
057                .setSnapshotPolicy(
058                    createDefaultPolicy(MvnVersionType.SNAPSHOT, false))
059                .build();
060
061    private MavenContext() {
062    }
063
064    private record SessionData(Settings settings,
065            RepositorySystem repositorySystem,
066            RepositorySystemSession repositorySession) {
067    }
068
069    /// Returns the singleton, lazily created session data.
070    ///
071    /// @return the session data
072    ///
073    @SuppressWarnings("PMD.AvoidSynchronizedStatement")
074    private static SessionData session() {
075        if (theSession != null) {
076            return theSession;
077        }
078        synchronized (MavenContext.class) {
079            if (theSession != null) {
080                return theSession;
081            }
082            return initSession();
083        }
084    }
085
086    private static SessionData initSession() {
087        // Settings
088        SettingsBuildingRequest settingsRequest
089            = new DefaultSettingsBuildingRequest().setUserSettingsFile(
090                new File(System.getProperty("user.home"), ".m2/settings.xml"));
091        SettingsBuilder settingsBuilder
092            = new DefaultSettingsBuilderFactory().newInstance();
093        SettingsBuildingResult settingsResult;
094        try {
095            settingsResult = settingsBuilder.build(settingsRequest);
096        } catch (SettingsBuildingException e) {
097            throw new BuildException().cause(e);
098        }
099        var settings = settingsResult.getEffectiveSettings();
100
101        // Repository system
102        @SuppressWarnings("PMD.CloseResource")
103        var repoSystem = new RepositorySystemSupplier().get();
104
105        // Repository system session
106        String localRepoPath = settings.getLocalRepository() != null
107            ? settings.getLocalRepository()
108            : System.getProperty("user.home") + "/.m2/repository";
109        @SuppressWarnings("PMD.CloseResource")
110        var session = new SessionBuilderSupplier(repoSystem).get()
111            .withLocalRepositoryBaseDirectories(Path.of(localRepoPath))
112            .setConfigProperty(
113                ConfigurableVersionSelector.CONFIG_PROP_SELECTION_STRATEGY,
114                ConfigurableVersionSelector.HIGHEST_SELECTION_STRATEGY)
115            .build();
116
117        // Combine
118        theSession = new SessionData(settings, repoSystem, session);
119        return theSession;
120    }
121
122    /// Repository system.
123    ///
124    /// @return the repository system
125    ///
126    public static RepositorySystem repositorySystem() {
127        return session().repositorySystem();
128    }
129
130    /// Repository session.
131    ///
132    /// @return the repository system session
133    ///
134    public static RepositorySystemSession repositorySession() {
135        return session().repositorySession();
136    }
137
138    /// Looks up the credentials for the specified server in `settings.xml`.
139    /// Invokes the consumer with the username and password if found.
140    ///
141    /// @param serverId the server id
142    /// @param consumer the consumer
143    /// @return true, if found
144    ///
145    public static boolean lookupCredentials(String serverId,
146            BiConsumer<String, String> consumer) {
147        return session().settings().getServers().stream()
148            .filter(s -> serverId.equals(s.getId())).findFirst().map(s -> {
149                consumer.accept(s.getUsername(), s.getPassword());
150                return true;
151            }).orElse(false);
152    }
153
154    /// Returns the [RemoteRepository] for Maven Central.
155    ///
156    /// @return the remote repository
157    ///
158    public static RemoteRepository mavenCentral() {
159        return MAVEN_CENTRAL_REPO;
160    }
161
162    /// Return the [RemoteRepository]s from the specified profile.
163    ///
164    /// @param profileId the profile id
165    /// @return the repositories
166    ///
167    public static Stream<RemoteRepository> repositories(String profileId) {
168        Objects.requireNonNull(profileId);
169        return repositories(session().settings(), profileId);
170    }
171
172    /// Return the [RemoteRepository]s from the specified settings and profile.
173    ///
174    /// @param settings the settings
175    /// @param profileId the profile id
176    /// @return the repositories
177    ///
178    public static Stream<RemoteRepository> repositories(Settings settings,
179            String profileId) {
180        Objects.requireNonNull(profileId);
181        if (!settings.getActiveProfiles().contains(profileId)) {
182            return Stream.empty();
183        }
184        Map<String, Profile> profiles = settings.getProfilesAsMap();
185        Profile profile = profiles.get(profileId);
186        if (profile == null) {
187            return Stream.empty();
188        }
189        return profile.getRepositories().stream().map(repo -> {
190            var builder = new RemoteRepository.Builder(repo.getId(),
191                "default", repo.getUrl())
192                    .setReleasePolicy(createPolicy(MvnVersionType.RELEASE,
193                        repo.getReleases()))
194                    .setSnapshotPolicy(createPolicy(MvnVersionType.SNAPSHOT,
195                        repo.getSnapshots()));
196            return builder.build();
197        });
198    }
199
200    /// Creates a policy for the specified type with reasonable defaults.
201    ///
202    /// @param type the type
203    /// @param enabled the enabled
204    /// @return the repository policy
205    ///
206    /* default */ static RepositoryPolicy createDefaultPolicy(
207            MvnVersionType type, boolean enabled) {
208        return createPolicy(type, enabled, null, null);
209    }
210
211    /// Creates a policy from settings data. Fills in reasonable defaults if
212    /// necessary.
213    ///
214    /// @param type the type
215    /// @param policy the policy data from settings
216    /// @return the repository policy
217    ///
218    /* default */ static RepositoryPolicy createPolicy(MvnVersionType type,
219            org.apache.maven.settings.RepositoryPolicy policy) {
220        if (policy == null) {
221            return createPolicy(type, false, null, null);
222        }
223        return createPolicy(type, policy.isEnabled(),
224            policy.getUpdatePolicy(), policy.getChecksumPolicy());
225    }
226
227    /// Creates a policy from settings data with the given details.
228    /// Fills in reasonable defaults for `null` values.
229    ///
230    /// @param type the type
231    /// @param enabled the enabled
232    /// @param updatePolicy the update policy
233    /// @param checksumPolicy the checksum policy
234    /// @return the repository policy
235    ///
236    /* default */ static RepositoryPolicy createPolicy(MvnVersionType type,
237            boolean enabled, String updatePolicy, String checksumPolicy) {
238        if (updatePolicy == null) {
239            updatePolicy = type == MvnVersionType.SNAPSHOT
240                ? RepositoryPolicy.UPDATE_POLICY_ALWAYS
241                : RepositoryPolicy.UPDATE_POLICY_NEVER;
242        }
243        if (checksumPolicy == null) {
244            checksumPolicy = RepositoryPolicy.CHECKSUM_POLICY_WARN;
245        }
246        return new RepositoryPolicy(enabled, updatePolicy, checksumPolicy);
247    }
248}