001/*
002 * Copyright 2017-2025 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven;
017
018import org.apache.maven.shared.invoker.InvocationOutputHandler;
019import org.apache.maven.shared.invoker.InvocationResult;
020import org.apache.maven.shared.utils.cli.CommandLineException;
021
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030/**
031 * An {@link InvocationResult} that includes the output and error. This allows callers to conditionally display the
032 * output depending on the exit code.
033 *
034 * @param result the invocation result.
035 * @param outputHandler the output handler.
036 */
037public record InvocationResultWithOutput(InvocationResult result,
038                                         PerGoalOutputHandler outputHandler) implements InvocationResult {
039
040    @Override
041    public CommandLineException getExecutionException() {
042        return result.getExecutionException();
043    }
044
045    @Override
046    public int getExitCode() {
047        return result.getExitCode();
048    }
049
050    /**
051     * An {@link InvocationOutputHandler} that can provide the output of each goals separately.
052     */
053    public static class PerGoalOutputHandler implements InvocationOutputHandler {
054
055        private static final Pattern GOAL_PATTERN = Pattern.compile(
056                "---\\s+([^:]+):([^:]+):([^\\s]+)\\s+\\(([^\\)]+)\\)\\s+@\\s+([^\\s]+)\\s+---");
057
058        // Matches the start or content of the trailing summary (BUILD SUCCESS/FAILURE, Total time, Finished at, etc.)
059        private static final Pattern BUILD_SUMMARY_PATTERN = Pattern.compile("^BUILD\\s(SUCCESS|FAILURE|ERROR)|Reactor\\sSummary");
060        private static final Pattern SEPARATOR_PATTERN = Pattern.compile("^-{72}$");
061        private static final Pattern PROJECT_HEADER_PATTERN = Pattern.compile("^-+<.*>-+$");
062
063        private final Map<String, List<String>> perGoalOutput = new LinkedHashMap<>();
064        private final List<String> fullOutput = new ArrayList<>();
065
066        private String currentGoal = null;
067        private boolean skippingSummary = false;
068        private boolean separatorFoundBefore = false;
069
070        @Override
071        public void consumeLine(String line) {
072            if (line == null) {
073                return;
074            }
075            // Strip standard Maven log prefixes like [INFO], [WARNING]
076            String cleanLine = line.replaceFirst("^\\[\\w+\\]\\s*", "").trim();
077            if (cleanLine.isEmpty()) {
078                return;
079            }
080            fullOutput.add(cleanLine);
081
082            // Once we start the build summary section — skip all further lines
083            if (skippingSummary) {
084                return;
085            }
086
087            if (separatorFoundBefore) {
088                separatorFoundBefore = false;
089                if (BUILD_SUMMARY_PATTERN.matcher(cleanLine).find()) {
090                    skippingSummary = true;
091                    return;
092                }
093            } else if (SEPARATOR_PATTERN.matcher(cleanLine).matches()) {
094                separatorFoundBefore = true;
095                return;
096            }
097
098            if (PROJECT_HEADER_PATTERN.matcher(cleanLine).find()) {
099                currentGoal = null;
100            }
101
102            Matcher goalMatcher = GOAL_PATTERN.matcher(cleanLine);
103            if (goalMatcher.find()) {
104                currentGoal = goalMatcher.group(1) + ":" + goalMatcher.group(3); // e.g., compiler:compile
105                perGoalOutput.computeIfAbsent(currentGoal, k -> new ArrayList<>()).add(cleanLine);
106            } else if (currentGoal != null) {
107                perGoalOutput.get(currentGoal).add(cleanLine);
108            }
109
110        }
111
112        /**
113         * @param pluginGoalKey plugin/goal formatted as pluginId:goal, e.g.: compiler:compile
114         * @return the output of the given plugin goal key, with log level prefixes and build summary stripped out.
115         */
116        public List<String> getOutput(String pluginGoalKey) {
117            return perGoalOutput.getOrDefault(pluginGoalKey, Collections.emptyList());
118        }
119
120        /**
121         * @return the complete output, without log prefixes or trailing summary lines.
122         */
123        public List<String> getOutput() {
124            return Collections.unmodifiableList(fullOutput);
125        }
126
127    }
128}