1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.cling.invoker.mvn;
20
21 import java.io.FileNotFoundException;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.util.ArrayList;
25 import java.util.Date;
26 import java.util.HashMap;
27 import java.util.LinkedHashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 import org.apache.maven.InternalErrorException;
34 import org.apache.maven.Maven;
35 import org.apache.maven.api.Constants;
36 import org.apache.maven.api.cli.Logger;
37 import org.apache.maven.api.cli.mvn.MavenInvoker;
38 import org.apache.maven.api.cli.mvn.MavenInvokerRequest;
39 import org.apache.maven.api.cli.mvn.MavenOptions;
40 import org.apache.maven.api.services.BuilderProblem;
41 import org.apache.maven.api.services.SettingsBuilderRequest;
42 import org.apache.maven.api.services.SettingsBuilderResult;
43 import org.apache.maven.api.services.Source;
44 import org.apache.maven.api.services.ToolchainsBuilder;
45 import org.apache.maven.api.services.ToolchainsBuilderRequest;
46 import org.apache.maven.api.services.ToolchainsBuilderResult;
47 import org.apache.maven.api.services.model.ModelProcessor;
48 import org.apache.maven.cli.CLIReportingUtils;
49 import org.apache.maven.cli.event.ExecutionEventLogger;
50 import org.apache.maven.cling.invoker.LookupInvoker;
51 import org.apache.maven.cling.invoker.ProtoLookup;
52 import org.apache.maven.cling.invoker.Utils;
53 import org.apache.maven.eventspy.internal.EventSpyDispatcher;
54 import org.apache.maven.exception.DefaultExceptionHandler;
55 import org.apache.maven.exception.ExceptionHandler;
56 import org.apache.maven.exception.ExceptionSummary;
57 import org.apache.maven.execution.DefaultMavenExecutionRequest;
58 import org.apache.maven.execution.ExecutionListener;
59 import org.apache.maven.execution.MavenExecutionRequest;
60 import org.apache.maven.execution.MavenExecutionRequestPopulator;
61 import org.apache.maven.execution.MavenExecutionResult;
62 import org.apache.maven.execution.ProfileActivation;
63 import org.apache.maven.execution.ProjectActivation;
64 import org.apache.maven.jline.MessageUtils;
65 import org.apache.maven.lifecycle.LifecycleExecutionException;
66 import org.apache.maven.logging.LoggingExecutionListener;
67 import org.apache.maven.logging.MavenTransferListener;
68 import org.apache.maven.project.MavenProject;
69 import org.codehaus.plexus.PlexusContainer;
70 import org.eclipse.aether.DefaultRepositoryCache;
71 import org.eclipse.aether.transfer.TransferListener;
72
73 import static java.util.Comparator.comparing;
74 import static org.apache.maven.cling.invoker.Utils.toProperties;
75
76 public abstract class DefaultMavenInvoker<
77 O extends MavenOptions,
78 R extends MavenInvokerRequest<O>,
79 C extends DefaultMavenInvoker.MavenContext<O, R, C>>
80 extends LookupInvoker<O, R, C> implements MavenInvoker<R> {
81
82 @SuppressWarnings("VisibilityModifier")
83 public static class MavenContext<
84 O extends MavenOptions,
85 R extends MavenInvokerRequest<O>,
86 C extends DefaultMavenInvoker.MavenContext<O, R, C>>
87 extends LookupInvokerContext<O, R, C> {
88 protected MavenContext(DefaultMavenInvoker<O, R, C> invoker, R invokerRequest) {
89 super(invoker, invokerRequest);
90 }
91
92 public MavenExecutionRequest mavenExecutionRequest;
93 public EventSpyDispatcher eventSpyDispatcher;
94 public MavenExecutionRequestPopulator mavenExecutionRequestPopulator;
95 public ToolchainsBuilder toolchainsBuilder;
96 public ModelProcessor modelProcessor;
97 public Maven maven;
98 }
99
100 public DefaultMavenInvoker(ProtoLookup protoLookup) {
101 super(protoLookup);
102 }
103
104 @Override
105 protected int execute(C context) throws Exception {
106 toolchains(context);
107 populateRequest(context, context.mavenExecutionRequest);
108 return doExecute(context);
109 }
110
111 @Override
112 protected void prepare(C context) throws Exception {
113
114 DefaultMavenExecutionRequest mavenExecutionRequest = new DefaultMavenExecutionRequest();
115 mavenExecutionRequest.setRepositoryCache(new DefaultRepositoryCache());
116 mavenExecutionRequest.setInteractiveMode(true);
117 mavenExecutionRequest.setCacheTransferError(false);
118 mavenExecutionRequest.setIgnoreInvalidArtifactDescriptor(true);
119 mavenExecutionRequest.setIgnoreMissingArtifactDescriptor(true);
120 mavenExecutionRequest.setRecursive(true);
121 mavenExecutionRequest.setReactorFailureBehavior(MavenExecutionRequest.REACTOR_FAIL_FAST);
122 mavenExecutionRequest.setStartTime(new Date());
123 mavenExecutionRequest.setLoggingLevel(MavenExecutionRequest.LOGGING_LEVEL_INFO);
124 mavenExecutionRequest.setDegreeOfConcurrency(1);
125 mavenExecutionRequest.setBuilderId("singlethreaded");
126
127 context.mavenExecutionRequest = mavenExecutionRequest;
128 }
129
130 @Override
131 protected void lookup(C context) throws Exception {
132 context.eventSpyDispatcher = context.lookup.lookup(EventSpyDispatcher.class);
133 context.mavenExecutionRequestPopulator = context.lookup.lookup(MavenExecutionRequestPopulator.class);
134 context.toolchainsBuilder = context.lookup.lookup(ToolchainsBuilder.class);
135 context.modelProcessor = context.lookup.lookup(ModelProcessor.class);
136 context.maven = context.lookup.lookup(Maven.class);
137 }
138
139 @Override
140 protected void init(C context) throws Exception {
141 MavenInvokerRequest<O> invokerRequest = context.invokerRequest;
142 Map<String, Object> data = new HashMap<>();
143 data.put("plexus", context.lookup.lookup(PlexusContainer.class));
144 data.put("workingDirectory", invokerRequest.cwd().toString());
145 data.put("systemProperties", toProperties(invokerRequest.systemProperties()));
146 data.put("userProperties", toProperties(invokerRequest.userProperties()));
147 data.put("versionProperties", CLIReportingUtils.getBuildProperties());
148 context.eventSpyDispatcher.init(() -> data);
149 }
150
151 @Override
152 protected void postCommands(C context) throws Exception {
153 super.postCommands(context);
154
155 R invokerRequest = context.invokerRequest;
156 Logger logger = context.logger;
157 if (invokerRequest.options().relaxedChecksums().orElse(false)) {
158 logger.info("Disabling strict checksum verification on all artifact downloads.");
159 } else if (invokerRequest.options().strictChecksums().orElse(false)) {
160 logger.info("Enabling strict checksum verification on all artifact downloads.");
161 }
162 }
163
164 @Override
165 protected void customizeSettingsRequest(C context, SettingsBuilderRequest settingsBuilderRequest) {
166 if (context.eventSpyDispatcher != null) {
167 context.eventSpyDispatcher.onEvent(settingsBuilderRequest);
168 }
169 }
170
171 @Override
172 protected void customizeSettingsResult(C context, SettingsBuilderResult settingsBuilderResult) throws Exception {
173 if (context.eventSpyDispatcher != null) {
174 context.eventSpyDispatcher.onEvent(settingsBuilderResult);
175 }
176 }
177
178 protected void toolchains(C context) throws Exception {
179 Path userToolchainsFile = null;
180
181 if (context.invokerRequest.options().altUserToolchains().isPresent()) {
182 userToolchainsFile = context.cwdResolver.apply(
183 context.invokerRequest.options().altUserToolchains().get());
184
185 if (!Files.isRegularFile(userToolchainsFile)) {
186 throw new FileNotFoundException(
187 "The specified user toolchains file does not exist: " + userToolchainsFile);
188 }
189 } else {
190 String userToolchainsFileStr =
191 context.invokerRequest.userProperties().get(Constants.MAVEN_USER_TOOLCHAINS);
192 if (userToolchainsFileStr != null) {
193 userToolchainsFile = context.cwdResolver.apply(userToolchainsFileStr);
194 }
195 }
196
197 Path installationToolchainsFile = null;
198
199 if (context.invokerRequest.options().altInstallationToolchains().isPresent()) {
200 installationToolchainsFile = context.cwdResolver.apply(
201 context.invokerRequest.options().altInstallationToolchains().get());
202
203 if (!Files.isRegularFile(installationToolchainsFile)) {
204 throw new FileNotFoundException(
205 "The specified installation toolchains file does not exist: " + installationToolchainsFile);
206 }
207 } else {
208 String installationToolchainsFileStr =
209 context.invokerRequest.userProperties().get(Constants.MAVEN_INSTALLATION_TOOLCHAINS);
210 if (installationToolchainsFileStr != null) {
211 installationToolchainsFile = context.cwdResolver.apply(installationToolchainsFileStr);
212 }
213 }
214
215 context.mavenExecutionRequest.setInstallationToolchainsFile(
216 installationToolchainsFile != null ? installationToolchainsFile.toFile() : null);
217 context.mavenExecutionRequest.setUserToolchainsFile(
218 userToolchainsFile != null ? userToolchainsFile.toFile() : null);
219
220 ToolchainsBuilderRequest toolchainsRequest = ToolchainsBuilderRequest.builder()
221 .session(context.session)
222 .installationToolchainsSource(
223 installationToolchainsFile != null && Files.isRegularFile(installationToolchainsFile)
224 ? Source.fromPath(installationToolchainsFile)
225 : null)
226 .userToolchainsSource(
227 userToolchainsFile != null && Files.isRegularFile(userToolchainsFile)
228 ? Source.fromPath(userToolchainsFile)
229 : null)
230 .build();
231
232 context.eventSpyDispatcher.onEvent(toolchainsRequest);
233
234 context.logger.debug("Reading installation toolchains from '" + installationToolchainsFile + "'");
235 context.logger.debug("Reading user toolchains from '" + userToolchainsFile + "'");
236
237 ToolchainsBuilderResult toolchainsResult = context.toolchainsBuilder.build(toolchainsRequest);
238
239 context.eventSpyDispatcher.onEvent(toolchainsResult);
240
241 context.mavenExecutionRequestPopulator.populateFromToolchains(
242 context.mavenExecutionRequest,
243 new org.apache.maven.toolchain.model.PersistedToolchains(toolchainsResult.getEffectiveToolchains()));
244
245 if (!toolchainsResult.getProblems().isEmpty()) {
246 context.logger.warn("");
247 context.logger.warn("Some problems were encountered while building the effective toolchains");
248
249 for (BuilderProblem problem : toolchainsResult.getProblems()) {
250 context.logger.warn(problem.getMessage() + " @ " + problem.getLocation());
251 }
252
253 context.logger.warn("");
254 }
255 }
256
257 @Override
258 protected void populateRequest(C context, MavenExecutionRequest request) throws Exception {
259 super.populateRequest(context, request);
260 if (context.invokerRequest.rootDirectory().isEmpty()) {
261
262 request.setMultiModuleProjectDirectory(
263 context.invokerRequest.topDirectory().toFile());
264 request.setRootDirectory(context.invokerRequest.topDirectory());
265 }
266
267 MavenOptions options = context.invokerRequest.options();
268 request.setNoSnapshotUpdates(options.suppressSnapshotUpdates().orElse(false));
269 request.setGoals(options.goals().orElse(List.of()));
270 request.setReactorFailureBehavior(determineReactorFailureBehaviour(context));
271 request.setRecursive(!options.nonRecursive().orElse(!request.isRecursive()));
272 request.setOffline(options.offline().orElse(request.isOffline()));
273 request.setUpdateSnapshots(options.updateSnapshots().orElse(false));
274 request.setGlobalChecksumPolicy(determineGlobalChecksumPolicy(context));
275
276 Path pom = determinePom(context);
277 if (pom != null) {
278 request.setPom(pom.toFile());
279 if (pom.getParent() != null) {
280 request.setBaseDirectory(pom.getParent().toFile());
281 }
282
283
284 if (context.invokerRequest.rootDirectory().isEmpty()) {
285 Path rootDirectory = Utils.findMandatoryRoot(context.invokerRequest.topDirectory());
286 request.setMultiModuleProjectDirectory(rootDirectory.toFile());
287 request.setRootDirectory(rootDirectory);
288 }
289 }
290
291 request.setTransferListener(
292 determineTransferListener(context, options.noTransferProgress().orElse(false)));
293 request.setExecutionListener(determineExecutionListener(context));
294
295 request.setResumeFrom(options.resumeFrom().orElse(null));
296 request.setResume(options.resume().orElse(false));
297 request.setMakeBehavior(determineMakeBehavior(context));
298 request.setCacheNotFound(options.cacheArtifactNotFound().orElse(true));
299 request.setCacheTransferError(false);
300
301 if (options.strictArtifactDescriptorPolicy().orElse(false)) {
302 request.setIgnoreMissingArtifactDescriptor(false);
303 request.setIgnoreInvalidArtifactDescriptor(false);
304 } else {
305 request.setIgnoreMissingArtifactDescriptor(true);
306 request.setIgnoreInvalidArtifactDescriptor(true);
307 }
308
309 request.setIgnoreTransitiveRepositories(
310 options.ignoreTransitiveRepositories().orElse(false));
311
312 performProjectActivation(context, request.getProjectActivation());
313 performProfileActivation(context, request.getProfileActivation());
314
315
316
317
318
319
320
321
322
323 if (context.invokerRequest.options().threads().isPresent()) {
324 int degreeOfConcurrency = calculateDegreeOfConcurrency(
325 context.invokerRequest.options().threads().get());
326 if (degreeOfConcurrency > 1) {
327 request.setBuilderId("multithreaded");
328 request.setDegreeOfConcurrency(degreeOfConcurrency);
329 }
330 }
331
332
333
334
335 if (context.invokerRequest.options().builder().isPresent()) {
336 request.setBuilderId(context.invokerRequest.options().builder().get());
337 }
338 }
339
340 protected Path determinePom(C context) {
341 Path current = context.invokerRequest.cwd();
342 if (context.invokerRequest.options().alternatePomFile().isPresent()) {
343 current = context.cwdResolver.apply(
344 context.invokerRequest.options().alternatePomFile().get());
345 }
346 if (context.modelProcessor != null) {
347 return context.modelProcessor.locateExistingPom(current);
348 } else {
349 return Files.isRegularFile(current) ? current : null;
350 }
351 }
352
353 protected String determineReactorFailureBehaviour(C context) {
354 MavenOptions mavenOptions = context.invokerRequest.options();
355 if (mavenOptions.failFast().isPresent()) {
356 return MavenExecutionRequest.REACTOR_FAIL_FAST;
357 } else if (mavenOptions.failAtEnd().isPresent()) {
358 return MavenExecutionRequest.REACTOR_FAIL_AT_END;
359 } else if (mavenOptions.failNever().isPresent()) {
360 return MavenExecutionRequest.REACTOR_FAIL_NEVER;
361 } else {
362 return MavenExecutionRequest.REACTOR_FAIL_FAST;
363 }
364 }
365
366 protected String determineGlobalChecksumPolicy(C context) {
367 MavenOptions mavenOptions = context.invokerRequest.options();
368 if (mavenOptions.strictChecksums().orElse(false)) {
369 return MavenExecutionRequest.CHECKSUM_POLICY_FAIL;
370 } else if (mavenOptions.relaxedChecksums().orElse(false)) {
371 return MavenExecutionRequest.CHECKSUM_POLICY_WARN;
372 } else {
373 return null;
374 }
375 }
376
377 protected ExecutionListener determineExecutionListener(C context) {
378 ExecutionListener listener = new ExecutionEventLogger(context.invokerRequest.messageBuilderFactory());
379 if (context.eventSpyDispatcher != null) {
380 listener = context.eventSpyDispatcher.chainListener(listener);
381 }
382 listener = new LoggingExecutionListener(listener, determineBuildEventListener(context));
383 return listener;
384 }
385
386 protected TransferListener determineTransferListener(C context, boolean noTransferProgress) {
387 TransferListener delegate = super.determineTransferListener(context, noTransferProgress);
388 return new MavenTransferListener(delegate, determineBuildEventListener(context));
389 }
390
391 protected String determineMakeBehavior(C context) {
392 MavenOptions mavenOptions = context.invokerRequest.options();
393 if (mavenOptions.alsoMake().isPresent()
394 && mavenOptions.alsoMakeDependents().isEmpty()) {
395 return MavenExecutionRequest.REACTOR_MAKE_UPSTREAM;
396 } else if (mavenOptions.alsoMake().isEmpty()
397 && mavenOptions.alsoMakeDependents().isPresent()) {
398 return MavenExecutionRequest.REACTOR_MAKE_DOWNSTREAM;
399 } else if (mavenOptions.alsoMake().isPresent()
400 && mavenOptions.alsoMakeDependents().isPresent()) {
401 return MavenExecutionRequest.REACTOR_MAKE_BOTH;
402 } else {
403 return null;
404 }
405 }
406
407 protected void performProjectActivation(C context, ProjectActivation projectActivation) {
408 MavenOptions mavenOptions = context.invokerRequest.options();
409 if (mavenOptions.projects().isPresent()
410 && !mavenOptions.projects().get().isEmpty()) {
411 List<String> optionValues = mavenOptions.projects().get();
412 for (final String optionValue : optionValues) {
413 for (String token : optionValue.split(",")) {
414 String selector = token.trim();
415 boolean active = true;
416 if (!selector.isEmpty()) {
417 if (selector.charAt(0) == '-' || selector.charAt(0) == '!') {
418 active = false;
419 selector = selector.substring(1);
420 } else if (token.charAt(0) == '+') {
421 selector = selector.substring(1);
422 }
423 }
424 boolean optional = false;
425 if (!selector.isEmpty() && selector.charAt(0) == '?') {
426 optional = true;
427 selector = selector.substring(1);
428 }
429 projectActivation.addProjectActivation(selector, active, optional);
430 }
431 }
432 }
433 }
434
435 protected void performProfileActivation(C context, ProfileActivation profileActivation) {
436 MavenOptions mavenOptions = context.invokerRequest.options();
437 if (mavenOptions.activatedProfiles().isPresent()
438 && !mavenOptions.activatedProfiles().get().isEmpty()) {
439 List<String> optionValues = mavenOptions.activatedProfiles().get();
440 for (final String optionValue : optionValues) {
441 for (String token : optionValue.split(",")) {
442 String profileId = token.trim();
443 boolean active = true;
444 if (!profileId.isEmpty()) {
445 if (profileId.charAt(0) == '-' || profileId.charAt(0) == '!') {
446 active = false;
447 profileId = profileId.substring(1);
448 } else if (token.charAt(0) == '+') {
449 profileId = profileId.substring(1);
450 }
451 }
452 boolean optional = false;
453 if (!profileId.isEmpty() && profileId.charAt(0) == '?') {
454 optional = true;
455 profileId = profileId.substring(1);
456 }
457 profileActivation.addProfileActivation(profileId, active, optional);
458 }
459 }
460 }
461 }
462
463 protected int doExecute(C context) throws Exception {
464 MavenExecutionRequest request = context.mavenExecutionRequest;
465
466 context.eventSpyDispatcher.onEvent(request);
467
468 MavenExecutionResult result;
469 try {
470 result = context.maven.execute(request);
471 context.eventSpyDispatcher.onEvent(result);
472 } finally {
473 context.eventSpyDispatcher.close();
474 }
475
476 if (result.hasExceptions()) {
477 ExceptionHandler handler = new DefaultExceptionHandler();
478 Map<String, String> references = new LinkedHashMap<>();
479 List<MavenProject> failedProjects = new ArrayList<>();
480
481 for (Throwable exception : result.getExceptions()) {
482 ExceptionSummary summary = handler.handleException(exception);
483 logSummary(context, summary, references, "");
484
485 if (exception instanceof LifecycleExecutionException) {
486 failedProjects.add(((LifecycleExecutionException) exception).getProject());
487 }
488 }
489
490 context.logger.error("");
491
492 if (!context.invokerRequest.options().showErrors().orElse(false)) {
493 context.logger.error("To see the full stack trace of the errors, re-run Maven with the '"
494 + MessageUtils.builder().strong("-e") + "' switch");
495 }
496 if (!context.invokerRequest.options().verbose().orElse(false)) {
497 context.logger.error("Re-run Maven using the '"
498 + MessageUtils.builder().strong("-X") + "' switch to enable verbose output");
499 }
500
501 if (!references.isEmpty()) {
502 context.logger.error("");
503 context.logger.error("For more information about the errors and possible solutions"
504 + ", please read the following articles:");
505
506 for (Map.Entry<String, String> entry : references.entrySet()) {
507 context.logger.error(MessageUtils.builder().strong(entry.getValue()) + " " + entry.getKey());
508 }
509 }
510
511 if (result.canResume()) {
512 logBuildResumeHint(context, "mvn [args] -r");
513 } else if (!failedProjects.isEmpty()) {
514 List<MavenProject> sortedProjects = result.getTopologicallySortedProjects();
515
516
517 failedProjects.sort(comparing(sortedProjects::indexOf));
518
519 MavenProject firstFailedProject = failedProjects.get(0);
520 if (!firstFailedProject.equals(sortedProjects.get(0))) {
521 String resumeFromSelector = getResumeFromSelector(sortedProjects, firstFailedProject);
522 logBuildResumeHint(context, "mvn [args] -rf " + resumeFromSelector);
523 }
524 }
525
526 if (context.invokerRequest.options().failNever().orElse(false)) {
527 context.logger.info("Build failures were ignored.");
528 return 0;
529 } else {
530 return 1;
531 }
532 } else {
533 return 0;
534 }
535 }
536
537 protected void logBuildResumeHint(C context, String resumeBuildHint) {
538 context.logger.error("");
539 context.logger.error("After correcting the problems, you can resume the build with the command");
540 context.logger.error(
541 MessageUtils.builder().a(" ").strong(resumeBuildHint).toString());
542 }
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561 protected String getResumeFromSelector(List<MavenProject> mavenProjects, MavenProject firstFailedProject) {
562 boolean hasOverlappingArtifactId = mavenProjects.stream()
563 .filter(project -> firstFailedProject.getArtifactId().equals(project.getArtifactId()))
564 .count()
565 > 1;
566
567 if (hasOverlappingArtifactId) {
568 return firstFailedProject.getGroupId() + ":" + firstFailedProject.getArtifactId();
569 }
570
571 return ":" + firstFailedProject.getArtifactId();
572 }
573
574 protected static final Pattern NEXT_LINE = Pattern.compile("\r?\n");
575
576 protected static final Pattern LAST_ANSI_SEQUENCE = Pattern.compile("(\u001B\\[[;\\d]*[ -/]*[@-~])[^\u001B]*$");
577
578 protected static final String ANSI_RESET = "\u001B\u005Bm";
579
580 protected void logSummary(C context, ExceptionSummary summary, Map<String, String> references, String indent) {
581 String referenceKey = "";
582
583 if (summary.getReference() != null && !summary.getReference().isEmpty()) {
584 referenceKey =
585 references.computeIfAbsent(summary.getReference(), k -> "[Help " + (references.size() + 1) + "]");
586 }
587
588 String msg = summary.getMessage();
589
590 if (!referenceKey.isEmpty()) {
591 if (msg.indexOf('\n') < 0) {
592 msg += " -> " + MessageUtils.builder().strong(referenceKey);
593 } else {
594 msg += "\n-> " + MessageUtils.builder().strong(referenceKey);
595 }
596 }
597
598 String[] lines = NEXT_LINE.split(msg);
599 String currentColor = "";
600
601 for (int i = 0; i < lines.length; i++) {
602
603 String line = currentColor + lines[i];
604
605
606 Matcher matcher = LAST_ANSI_SEQUENCE.matcher(line);
607 String nextColor = "";
608 if (matcher.find()) {
609 nextColor = matcher.group(1);
610 if (ANSI_RESET.equals(nextColor)) {
611
612 nextColor = "";
613 }
614 }
615
616
617 line = indent + line + ("".equals(nextColor) ? "" : ANSI_RESET);
618
619 if ((i == lines.length - 1)
620 && (context.invokerRequest.options().showErrors().orElse(false)
621 || (summary.getException() instanceof InternalErrorException))) {
622 context.logger.error(line, summary.getException());
623 } else {
624 context.logger.error(line);
625 }
626
627 currentColor = nextColor;
628 }
629
630 indent += " ";
631
632 for (ExceptionSummary child : summary.getChildren()) {
633 logSummary(context, child, references, indent);
634 }
635 }
636 }