DefaultBuildInvocation.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.internal.build;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.apache.maven.api.BuildInvocation;
import org.apache.maven.api.MakeBehavior;
import org.apache.maven.api.ReactorFailureBehavior;
import org.apache.maven.execution.MavenSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Internal immutable implementation of {@link BuildInvocation}.
 *
 * @param goals                  requested goals/phases
 * @param userProperties         user properties with sensitive values redacted
 * @param systemInfo             curated system properties for reproducibility
 * @param activeProfiles         explicitly activated profiles
 * @param selectedProjects       projects selected via {@code -pl}
 * @param excludedProjects       projects excluded via {@code -pl !module}
 * @param makeBehavior           reactor make behavior ({@code -am}/{@code -amd}), or {@code null}
 * @param resumeFrom             resume-from selector, or {@code null}
 * @param reactorFailureBehavior reactor failure behavior
 * @param offline                whether offline mode was active
 * @param updateSnapshots        whether snapshot updates were forced
 * @param batchMode              whether batch mode was active
 * @param recursive              whether recursive processing is enabled
 * @param resume                 whether resume mode was active
 * @param threads                degree of concurrency
 */
public record DefaultBuildInvocation(
        List<String> goals,
        Map<String, String> userProperties,
        Map<String, String> systemInfo,
        List<String> activeProfiles,
        List<String> selectedProjects,
        List<String> excludedProjects,
        MakeBehavior makeBehavior,
        String resumeFrom,
        ReactorFailureBehavior reactorFailureBehavior,
        boolean offline,
        boolean updateSnapshots,
        boolean batchMode,
        boolean recursive,
        boolean resume,
        int threads)
        implements BuildInvocation {

    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultBuildInvocation.class);

    private static final Set<String> SENSITIVE_KEY_FRAGMENTS =
            Set.of("password", "passwd", "secret", "token", "apikey", "api_key", "credential", "passphrase");

    public DefaultBuildInvocation {
        goals = List.copyOf(goals);
        userProperties = Map.copyOf(userProperties);
        systemInfo = Map.copyOf(systemInfo);
        activeProfiles = List.copyOf(activeProfiles);
        selectedProjects = List.copyOf(selectedProjects);
        excludedProjects = List.copyOf(excludedProjects);
    }

    /**
     * Creates a {@link BuildInvocation} snapshot from the current session state.
     *
     * @param session the Maven session
     * @return a new immutable build invocation instance
     */
    public static BuildInvocation from(MavenSession session) {
        var request = session.getRequest();

        List<String> goals = request.getGoals() != null ? List.copyOf(request.getGoals()) : List.of();

        Map<String, String> userProps = new LinkedHashMap<>();
        if (request.getUserProperties() != null) {
            request.getUserProperties().forEach((k, v) -> {
                String key = String.valueOf(k);
                String value = isSensitiveKey(key) ? "***" : String.valueOf(v);
                userProps.put(key, value);
            });
        }

        Map<String, String> sysInfo = new TreeMap<>();
        addSystemProp(sysInfo, "os.name");
        addSystemProp(sysInfo, "os.arch");
        addSystemProp(sysInfo, "os.version");
        addSystemProp(sysInfo, "java.vendor");
        addSystemProp(sysInfo, "java.vm.name");
        addSystemProp(sysInfo, "java.vm.version");
        addSystemProp(sysInfo, "maven.home");
        addSystemProp(sysInfo, "user.home");
        addSystemProp(sysInfo, "user.name");
        sysInfo.put("available.processors", String.valueOf(Runtime.getRuntime().availableProcessors()));

        List<String> activeProfiles =
                request.getActiveProfiles() != null ? List.copyOf(request.getActiveProfiles()) : List.of();

        List<String> selectedProjects =
                request.getSelectedProjects() != null ? List.copyOf(request.getSelectedProjects()) : List.of();

        List<String> excludedProjects =
                request.getExcludedProjects() != null ? List.copyOf(request.getExcludedProjects()) : List.of();

        MakeBehavior makeBehavior = null;
        if (request.getMakeBehavior() != null) {
            try {
                makeBehavior = MakeBehavior.fromString(request.getMakeBehavior());
            } catch (IllegalArgumentException e) {
                LOGGER.debug("Unknown make behavior '{}', ignoring", request.getMakeBehavior());
            }
        }

        String resumeFrom = request.getResumeFrom();

        ReactorFailureBehavior failureBehavior = ReactorFailureBehavior.FAIL_FAST;
        if (request.getReactorFailureBehavior() != null) {
            try {
                failureBehavior = ReactorFailureBehavior.fromString(request.getReactorFailureBehavior());
            } catch (IllegalArgumentException e) {
                LOGGER.debug(
                        "Unknown reactor failure behavior '{}', defaulting to FAIL_FAST",
                        request.getReactorFailureBehavior());
            }
        }

        return new DefaultBuildInvocation(
                goals,
                Map.copyOf(userProps),
                Map.copyOf(sysInfo),
                activeProfiles,
                selectedProjects,
                excludedProjects,
                makeBehavior,
                resumeFrom,
                failureBehavior,
                request.isOffline(),
                request.isUpdateSnapshots(),
                !request.isInteractiveMode(),
                request.isRecursive(),
                request.isResume(),
                request.getDegreeOfConcurrency());
    }

    static boolean isSensitiveKey(String key) {
        String lower = key.toLowerCase(Locale.ROOT);
        for (String fragment : SENSITIVE_KEY_FRAGMENTS) {
            if (lower.contains(fragment)) {
                return true;
            }
        }
        return false;
    }

    private static void addSystemProp(Map<String, String> map, String key) {
        String value = System.getProperty(key);
        if (value != null) {
            map.put(key, value);
        }
    }
}