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}