View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.cli.event;
20  
21  import java.io.File;
22  import java.nio.file.Path;
23  import java.util.ArrayList;
24  import java.util.List;
25  import java.util.Objects;
26  
27  import org.apache.maven.execution.AbstractExecutionListener;
28  import org.apache.maven.execution.BuildFailure;
29  import org.apache.maven.execution.BuildSuccess;
30  import org.apache.maven.execution.BuildSummary;
31  import org.apache.maven.execution.ExecutionEvent;
32  import org.apache.maven.execution.MavenExecutionResult;
33  import org.apache.maven.execution.MavenSession;
34  import org.apache.maven.jline.MessageUtils;
35  import org.apache.maven.message.MessageBuilder;
36  import org.apache.maven.plugin.MojoExecution;
37  import org.apache.maven.plugin.descriptor.MojoDescriptor;
38  import org.apache.maven.project.MavenProject;
39  import org.codehaus.plexus.util.StringUtils;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  import static org.apache.maven.cli.CLIReportingUtils.formatDuration;
44  import static org.apache.maven.cli.CLIReportingUtils.formatTimestamp;
45  import static org.apache.maven.jline.MessageUtils.builder;
46  
47  /**
48   * Logs execution events to logger, eventually user-supplied.
49   *
50   * @author Benjamin Bentmann
51   */
52  public class ExecutionEventLogger extends AbstractExecutionListener {
53      private final Logger logger;
54  
55      private static final int LINE_LENGTH = 72;
56      private static final int MAX_PADDED_BUILD_TIME_DURATION_LENGTH = 9;
57      private static final int MAX_PROJECT_NAME_LENGTH = 52;
58  
59      private int totalProjects;
60      private volatile int currentVisitedProjectCount;
61  
62      public ExecutionEventLogger() {
63          logger = LoggerFactory.getLogger(ExecutionEventLogger.class);
64      }
65  
66      // TODO should we deprecate?
67      public ExecutionEventLogger(Logger logger) {
68          this.logger = Objects.requireNonNull(logger, "logger cannot be null");
69      }
70  
71      private static String chars(char c, int count) {
72          StringBuilder buffer = new StringBuilder(count);
73  
74          for (int i = count; i > 0; i--) {
75              buffer.append(c);
76          }
77  
78          return buffer.toString();
79      }
80  
81      private void infoLine(char c) {
82          infoMain(chars(c, LINE_LENGTH));
83      }
84  
85      private void infoMain(String msg) {
86          logger.info(builder().strong(msg).toString());
87      }
88  
89      @Override
90      public void projectDiscoveryStarted(ExecutionEvent event) {
91          if (logger.isInfoEnabled()) {
92              logger.info("Scanning for projects...");
93          }
94      }
95  
96      @Override
97      public void sessionStarted(ExecutionEvent event) {
98          if (logger.isInfoEnabled() && event.getSession().getProjects().size() > 1) {
99              infoLine('-');
100 
101             infoMain("Reactor Build Order:");
102 
103             logger.info("");
104 
105             final List<MavenProject> projects = event.getSession().getProjects();
106             for (MavenProject project : projects) {
107                 int len = LINE_LENGTH
108                         - project.getName().length()
109                         - project.getPackaging().length()
110                         - 2;
111                 logger.info("{}{}[{}]", project.getName(), chars(' ', (len > 0) ? len : 1), project.getPackaging());
112             }
113 
114             totalProjects = projects.size();
115         }
116     }
117 
118     @Override
119     public void sessionEnded(ExecutionEvent event) {
120         if (logger.isInfoEnabled()) {
121             if (event.getSession().getProjects().size() > 1) {
122                 logReactorSummary(event.getSession());
123             }
124 
125             logResult(event.getSession());
126 
127             logStats(event.getSession());
128 
129             infoLine('-');
130         }
131     }
132 
133     private boolean isSingleVersionedReactor(MavenSession session) {
134         boolean result = true;
135 
136         MavenProject topProject = session.getTopLevelProject();
137         List<MavenProject> sortedProjects = session.getProjectDependencyGraph().getSortedProjects();
138         for (MavenProject mavenProject : sortedProjects) {
139             if (!topProject.getVersion().equals(mavenProject.getVersion())) {
140                 result = false;
141                 break;
142             }
143         }
144 
145         return result;
146     }
147 
148     private void logReactorSummary(MavenSession session) {
149         boolean isSingleVersion = isSingleVersionedReactor(session);
150 
151         infoLine('-');
152 
153         StringBuilder summary = new StringBuilder("Reactor Summary");
154         if (isSingleVersion) {
155             summary.append(" for ");
156             summary.append(session.getTopLevelProject().getName());
157             summary.append(" ");
158             summary.append(session.getTopLevelProject().getVersion());
159         }
160         summary.append(":");
161         infoMain(summary.toString());
162 
163         logger.info("");
164 
165         MavenExecutionResult result = session.getResult();
166 
167         List<MavenProject> projects = session.getProjects();
168 
169         String skippedMessage = builder().warning("SKIPPED").build();
170         String successMessage = builder().success("SUCCESS").build();
171         String failureMessage = builder().failure("FAILURE").build();
172         String unknownMessage = builder().warning("UNKNOWN").build();
173 
174         List<ReactorSummaryEntry> entries = new ArrayList<>(projects.size());
175         for (MavenProject project : projects) {
176             BuildSummary buildSummary = result.getBuildSummary(project);
177 
178             String statusMessage;
179             int group;
180             if (buildSummary instanceof BuildSuccess) {
181                 statusMessage = successMessage;
182                 group = 1;
183             } else if (buildSummary instanceof BuildFailure) {
184                 statusMessage = failureMessage;
185                 group = 2;
186             } else if (buildSummary == null) {
187                 statusMessage = skippedMessage;
188                 group = 0;
189             } else {
190                 statusMessage = unknownMessage;
191                 group = 2;
192             }
193             entries.add(new ReactorSummaryEntry(project, buildSummary, group, statusMessage));
194         }
195 
196         ReactorSummaryRequest request = new ReactorSummaryRequest(entries, isSingleVersion);
197 
198         logReactorSummaryGroup(request, 0);
199         logReactorSummaryGroup(request, 1);
200         logReactorSummaryGroup(request, 2);
201     }
202 
203     private void logReactorSummaryGroup(ReactorSummaryRequest request, int group) {
204         StringBuilder buffer = new StringBuilder(128);
205 
206         for (ReactorSummaryEntry entry : request.getEntries()) {
207             if (entry.getGroup() != group) {
208                 continue;
209             }
210 
211             buffer.append(entry.getProject().getName());
212             buffer.append(' ');
213 
214             if (!request.isSingleVersion()) {
215                 buffer.append(entry.getProject().getVersion());
216                 buffer.append(' ');
217             }
218 
219             if (buffer.length() <= MAX_PROJECT_NAME_LENGTH) {
220                 while (buffer.length() < MAX_PROJECT_NAME_LENGTH) {
221                     buffer.append('.');
222                 }
223                 buffer.append(' ');
224             }
225 
226             buffer.append(entry.getStatusMessage());
227             if (entry.getBuildSummary() != null) {
228                 formatBuildTime(buffer, entry.getBuildSummary());
229             }
230 
231             if (entry.getBuildSummary() instanceof BuildFailure) {
232                 logger.error(buffer.toString());
233             } else {
234                 logger.info(buffer.toString());
235             }
236             buffer.setLength(0);
237         }
238     }
239 
240     private static final class ReactorSummaryRequest {
241         private final List<ReactorSummaryEntry> entries;
242         private final boolean singleVersion;
243 
244         private ReactorSummaryRequest(List<ReactorSummaryEntry> entries, boolean singleVersion) {
245             this.entries = entries;
246             this.singleVersion = singleVersion;
247         }
248 
249         private List<ReactorSummaryEntry> getEntries() {
250             return entries;
251         }
252 
253         private boolean isSingleVersion() {
254             return singleVersion;
255         }
256     }
257 
258     private static final class ReactorSummaryEntry {
259         private final MavenProject project;
260         private final BuildSummary buildSummary;
261         private final int group;
262         private final String statusMessage;
263 
264         private ReactorSummaryEntry(MavenProject project, BuildSummary buildSummary, int group, String statusMessage) {
265             this.project = project;
266             this.buildSummary = buildSummary;
267             this.group = group;
268             this.statusMessage = statusMessage;
269         }
270 
271         private MavenProject getProject() {
272             return project;
273         }
274 
275         private BuildSummary getBuildSummary() {
276             return buildSummary;
277         }
278 
279         private int getGroup() {
280             return group;
281         }
282 
283         private String getStatusMessage() {
284             return statusMessage;
285         }
286     }
287 
288     private void formatBuildTime(StringBuilder buffer, BuildSummary buildSummary) {
289         buffer.append(" [");
290         String buildTimeDuration = formatDuration(buildSummary.getTime());
291         int padSize = MAX_PADDED_BUILD_TIME_DURATION_LENGTH - buildTimeDuration.length();
292         if (padSize > 0) {
293             buffer.append(chars(' ', padSize));
294         }
295         buffer.append(buildTimeDuration);
296         buffer.append(']');
297     }
298 
299     private void logResult(MavenSession session) {
300         infoLine('-');
301         MessageBuilder buffer = MessageUtils.builder();
302 
303         boolean failure = session.getResult().hasExceptions();
304         if (failure) {
305             buffer.failure("BUILD FAILURE");
306         } else {
307             buffer.success("BUILD SUCCESS");
308         }
309         if (failure) {
310             logger.error(buffer.toString());
311         } else {
312             logger.info(buffer.toString());
313         }
314     }
315 
316     private void logStats(MavenSession session) {
317         infoLine('-');
318 
319         long finish = System.currentTimeMillis();
320 
321         long time = finish - session.getRequest().getStartTime().getTime();
322 
323         String wallClock = session.getRequest().getDegreeOfConcurrency() > 1 ? " (Wall Clock)" : "";
324 
325         logger.info("Total time:  {}{}", formatDuration(time), wallClock);
326 
327         logger.info("Finished at: {}", formatTimestamp(finish));
328     }
329 
330     @Override
331     public void projectSkipped(ExecutionEvent event) {
332         if (logger.isInfoEnabled()) {
333             logger.info("");
334             infoLine('-');
335 
336             infoMain("Skipping " + event.getProject().getName());
337             logger.info("This project has been banned from the build due to previous failures.");
338 
339             infoLine('-');
340         }
341     }
342 
343     @Override
344     public void projectStarted(ExecutionEvent event) {
345         if (logger.isInfoEnabled()) {
346             MavenProject project = event.getProject();
347 
348             logger.info("");
349 
350             // -------< groupId:artifactId >-------
351             String projectKey = project.getGroupId() + ':' + project.getArtifactId();
352 
353             final String preHeader = "--< ";
354             final String postHeader = " >--";
355 
356             final int headerLen = preHeader.length() + projectKey.length() + postHeader.length();
357 
358             String prefix = chars('-', Math.max(0, (LINE_LENGTH - headerLen) / 2)) + preHeader;
359 
360             String suffix = postHeader
361                     + chars('-', Math.max(0, LINE_LENGTH - headerLen - prefix.length() + preHeader.length()));
362 
363             logger.info(
364                     builder().strong(prefix).project(projectKey).strong(suffix).toString());
365 
366             // Building Project Name Version    [i/n]
367             String building = "Building " + event.getProject().getName() + " "
368                     + event.getProject().getVersion();
369 
370             if (totalProjects <= 1) {
371                 infoMain(building);
372             } else {
373                 // display progress [i/n]
374                 int number;
375                 synchronized (this) {
376                     number = ++currentVisitedProjectCount;
377                 }
378                 String progress = " [" + number + '/' + totalProjects + ']';
379 
380                 int pad = LINE_LENGTH - building.length() - progress.length();
381 
382                 infoMain(building + ((pad > 0) ? chars(' ', pad) : "") + progress);
383             }
384 
385             // path to pom.xml
386             File currentPom = project.getFile();
387             if (currentPom != null) {
388                 MavenSession session = event.getSession();
389                 Path topLevelBasedir = session.getTopLevelProject().getBasedir().toPath();
390                 Path current = currentPom.toPath().toAbsolutePath().normalize();
391                 if (current.startsWith(topLevelBasedir)) {
392                     current = topLevelBasedir.relativize(current);
393                 }
394                 logger.info("  from " + current);
395             }
396 
397             // ----------[ packaging ]----------
398             prefix =
399                     chars('-', Math.max(0, (LINE_LENGTH - project.getPackaging().length() - 4) / 2));
400             suffix = chars('-', Math.max(0, LINE_LENGTH - project.getPackaging().length() - 4 - prefix.length()));
401             infoMain(prefix + "[ " + project.getPackaging() + " ]" + suffix);
402         }
403     }
404 
405     @Override
406     public void mojoSkipped(ExecutionEvent event) {
407         if (logger.isWarnEnabled()) {
408             logger.warn(
409                     "Goal {} requires online mode for execution but Maven is currently offline, skipping",
410                     event.getMojoExecution().getGoal());
411         }
412     }
413 
414     /**
415      * <pre>--- mojo-artifactId:version:goal (mojo-executionId) @ project-artifactId ---</pre>
416      */
417     @Override
418     public void mojoStarted(ExecutionEvent event) {
419         if (logger.isInfoEnabled()) {
420             logger.info("");
421 
422             MessageBuilder buffer = builder().strong("--- ");
423             append(buffer, event.getMojoExecution());
424             append(buffer, event.getProject());
425             buffer.strong(" ---");
426 
427             logger.info(buffer.toString());
428         }
429     }
430 
431     // CHECKSTYLE_OFF: LineLength
432     /**
433      * <pre>&gt;&gt;&gt; mojo-artifactId:version:goal (mojo-executionId) &gt; :forked-goal @ project-artifactId &gt;&gt;&gt;</pre>
434      * <pre>&gt;&gt;&gt; mojo-artifactId:version:goal (mojo-executionId) &gt; [lifecycle]phase @ project-artifactId &gt;&gt;&gt;</pre>
435      */
436     // CHECKSTYLE_ON: LineLength
437     @Override
438     public void forkStarted(ExecutionEvent event) {
439         if (logger.isInfoEnabled()) {
440             logger.info("");
441 
442             MessageBuilder buffer = builder().strong(">>> ");
443             append(buffer, event.getMojoExecution());
444             buffer.strong(" > ");
445             appendForkInfo(buffer, event.getMojoExecution().getMojoDescriptor());
446             append(buffer, event.getProject());
447             buffer.strong(" >>>");
448 
449             logger.info(buffer.toString());
450         }
451     }
452 
453     // CHECKSTYLE_OFF: LineLength
454     /**
455      * <pre>&lt;&lt;&lt; mojo-artifactId:version:goal (mojo-executionId) &lt; :forked-goal @ project-artifactId &lt;&lt;&lt;</pre>
456      * <pre>&lt;&lt;&lt; mojo-artifactId:version:goal (mojo-executionId) &lt; [lifecycle]phase @ project-artifactId &lt;&lt;&lt;</pre>
457      */
458     // CHECKSTYLE_ON: LineLength
459     @Override
460     public void forkSucceeded(ExecutionEvent event) {
461         if (logger.isInfoEnabled()) {
462             logger.info("");
463 
464             MessageBuilder buffer = builder().strong("<<< ");
465             append(buffer, event.getMojoExecution());
466             buffer.strong(" < ");
467             appendForkInfo(buffer, event.getMojoExecution().getMojoDescriptor());
468             append(buffer, event.getProject());
469             buffer.strong(" <<<");
470 
471             logger.info(buffer.toString());
472 
473             logger.info("");
474         }
475     }
476 
477     private void append(MessageBuilder buffer, MojoExecution me) {
478         String prefix = me.getMojoDescriptor().getPluginDescriptor().getGoalPrefix();
479         if (StringUtils.isEmpty(prefix)) {
480             prefix = me.getGroupId() + ":" + me.getArtifactId();
481         }
482         buffer.mojo(prefix + ':' + me.getVersion() + ':' + me.getGoal());
483         if (me.getExecutionId() != null) {
484             buffer.a(' ').strong('(' + me.getExecutionId() + ')');
485         }
486     }
487 
488     private void appendForkInfo(MessageBuilder buffer, MojoDescriptor md) {
489         StringBuilder buff = new StringBuilder();
490         if (StringUtils.isNotEmpty(md.getExecutePhase())) {
491             // forked phase
492             if (StringUtils.isNotEmpty(md.getExecuteLifecycle())) {
493                 buff.append('[');
494                 buff.append(md.getExecuteLifecycle());
495                 buff.append(']');
496             }
497             buff.append(md.getExecutePhase());
498         } else {
499             // forked goal
500             buff.append(':');
501             buff.append(md.getExecuteGoal());
502         }
503         buffer.strong(buff.toString());
504     }
505 
506     private void append(MessageBuilder buffer, MavenProject project) {
507         buffer.a(" @ ").project(project.getArtifactId());
508     }
509 
510     @Override
511     public void forkedProjectStarted(ExecutionEvent event) {
512         if (logger.isInfoEnabled()
513                 && event.getMojoExecution().getForkedExecutions().size() > 1) {
514             logger.info("");
515             infoLine('>');
516 
517             infoMain("Forking " + event.getProject().getName() + " "
518                     + event.getProject().getVersion());
519 
520             infoLine('>');
521         }
522     }
523 }