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}