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.report.projectinfo.dependencies.renderer;
20  
21  import javax.swing.text.html.HTML.Attribute;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.PrintWriter;
26  import java.io.StringWriter;
27  import java.text.DecimalFormat;
28  import java.text.DecimalFormatSymbols;
29  import java.text.FieldPosition;
30  import java.text.MessageFormat;
31  import java.util.ArrayList;
32  import java.util.Collections;
33  import java.util.Comparator;
34  import java.util.HashMap;
35  import java.util.HashSet;
36  import java.util.Iterator;
37  import java.util.List;
38  import java.util.Locale;
39  import java.util.Map;
40  import java.util.Set;
41  import java.util.SortedSet;
42  import java.util.TreeSet;
43  
44  import org.apache.maven.artifact.Artifact;
45  import org.apache.maven.doxia.sink.Sink;
46  import org.apache.maven.doxia.sink.SinkEventAttributes;
47  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
48  import org.apache.maven.doxia.util.HtmlTools;
49  import org.apache.maven.model.License;
50  import org.apache.maven.plugin.logging.Log;
51  import org.apache.maven.project.MavenProject;
52  import org.apache.maven.project.ProjectBuilder;
53  import org.apache.maven.project.ProjectBuildingException;
54  import org.apache.maven.project.ProjectBuildingRequest;
55  import org.apache.maven.report.projectinfo.AbstractProjectInfoRenderer;
56  import org.apache.maven.report.projectinfo.LicenseMapping;
57  import org.apache.maven.report.projectinfo.ProjectInfoReportUtils;
58  import org.apache.maven.report.projectinfo.dependencies.Dependencies;
59  import org.apache.maven.report.projectinfo.dependencies.DependenciesReportConfiguration;
60  import org.apache.maven.report.projectinfo.dependencies.RepositoryUtils;
61  import org.apache.maven.report.projectinfo.dependencies.renderer.DependenciesRenderer.TotalCell.SummaryTableRowOrder;
62  import org.apache.maven.repository.RepositorySystem;
63  import org.apache.maven.shared.dependency.graph.DependencyNode;
64  import org.apache.maven.shared.jar.JarData;
65  import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException;
66  import org.codehaus.plexus.i18n.I18N;
67  import org.codehaus.plexus.util.StringUtils;
68  
69  /**
70   * Renderer the dependencies report.
71   *
72   * @version $Id$
73   * @since 2.1
74   */
75  public class DependenciesRenderer extends AbstractProjectInfoRenderer {
76  
77      /** URL for the 'icon_info_sml.gif' image */
78      private static final String IMG_INFO_URL = "./images/icon_info_sml.gif";
79  
80      /** URL for the 'close.gif' image */
81      private static final String IMG_CLOSE_URL = "./images/close.gif";
82  
83      private static final Set<String> JAR_SUBTYPE;
84  
85      private final DependencyNode dependencyNode;
86  
87      private final Dependencies dependencies;
88  
89      private final DependenciesReportConfiguration configuration;
90  
91      private final Log log;
92  
93      private final RepositoryUtils repoUtils;
94  
95      /** Used to format file length values */
96      private final DecimalFormat fileLengthDecimalFormat;
97  
98      private final MessageFormat javaVersionFormat =
99              new MessageFormat("{0,choice,0#|1.1#{0,number,0.0}|9#{0,number,0}}", Locale.ROOT);
100     /**
101      * @since 2.1.1
102      */
103     private int section;
104 
105     /** Counter for unique IDs that is consistent across generations. */
106     private int idCounter = 0;
107 
108     /**
109      * Will be filled with license name / set of projects.
110      */
111     private Map<String, Object> licenseMap = new HashMap<String, Object>() {
112         private static final long serialVersionUID = 1L;
113 
114         /** {@inheritDoc} */
115         @Override
116         public Object put(String key, Object value) {
117             // handle multiple values as a set to avoid duplicates
118             @SuppressWarnings("unchecked")
119             SortedSet<Object> valueList = (SortedSet<Object>) get(key);
120             if (valueList == null) {
121                 valueList = new TreeSet<>();
122             }
123             valueList.add(value);
124             return super.put(key, valueList);
125         }
126     };
127 
128     private final RepositorySystem repositorySystem;
129 
130     private final ProjectBuilder projectBuilder;
131 
132     private final ProjectBuildingRequest buildingRequest;
133 
134     private final Map<String, String> licenseMappings;
135 
136     static {
137         Set<String> jarSubtype = new HashSet<>();
138         jarSubtype.add("jar");
139         jarSubtype.add("war");
140         jarSubtype.add("ear");
141         jarSubtype.add("sar");
142         jarSubtype.add("rar");
143         jarSubtype.add("par");
144         jarSubtype.add("ejb");
145         JAR_SUBTYPE = Collections.unmodifiableSet(jarSubtype);
146     }
147 
148     /**
149      * Default constructor.
150      *
151      * @param sink {@link Sink}
152      * @param locale {@link Locale}
153      * @param i18n {@link I18N}
154      * @param log {@link Log}
155      * @param dependencies {@link Dependencies}
156      * @param dependencyTreeNode {@link DependencyNode}
157      * @param config {@link DependenciesReportConfiguration}
158      * @param repoUtils {@link RepositoryUtils}
159      * @param repositorySystem {@link RepositorySystem}
160      * @param projectBuilder {@link ProjectBuilder}
161      * @param buildingRequest {@link ProjectBuildingRequest}
162      * @param licenseMappings {@link LicenseMapping}
163      */
164     public DependenciesRenderer(
165             Sink sink,
166             Locale locale,
167             I18N i18n,
168             Log log,
169             Dependencies dependencies,
170             DependencyNode dependencyTreeNode,
171             DependenciesReportConfiguration config,
172             RepositoryUtils repoUtils,
173             RepositorySystem repositorySystem,
174             ProjectBuilder projectBuilder,
175             ProjectBuildingRequest buildingRequest,
176             Map<String, String> licenseMappings) {
177         super(sink, i18n, locale);
178 
179         this.log = log;
180         this.dependencies = dependencies;
181         this.dependencyNode = dependencyTreeNode;
182         this.repoUtils = repoUtils;
183         this.configuration = config;
184         this.repositorySystem = repositorySystem;
185         this.projectBuilder = projectBuilder;
186         this.buildingRequest = buildingRequest;
187         this.licenseMappings = licenseMappings;
188         this.fileLengthDecimalFormat = new FileDecimalFormat(i18n, locale);
189         this.fileLengthDecimalFormat.setDecimalFormatSymbols(new DecimalFormatSymbols(locale));
190     }
191 
192     @Override
193     protected String getI18Nsection() {
194         return "dependencies";
195     }
196 
197     // ----------------------------------------------------------------------
198     // Public methods
199     // ----------------------------------------------------------------------
200 
201     @Override
202     protected void renderBody() {
203         // Dependencies report
204 
205         if (!dependencies.hasDependencies()) {
206             startSection(getTitle());
207 
208             paragraph(getI18nString("nolist"));
209 
210             endSection();
211 
212             return;
213         }
214 
215         // === Section: Project Dependencies.
216         renderSectionProjectDependencies();
217 
218         // === Section: Project Transitive Dependencies.
219         renderSectionProjectTransitiveDependencies();
220 
221         // === Section: Project Dependency Graph.
222         renderSectionProjectDependencyGraph();
223 
224         // === Section: Licenses
225         renderSectionDependencyLicenseListing();
226 
227         if (configuration.getDependencyDetailsEnabled()) {
228             // === Section: Dependency File Details.
229             renderSectionDependencyFileDetails();
230         }
231     }
232 
233     // ----------------------------------------------------------------------
234     // Protected methods
235     // ----------------------------------------------------------------------
236 
237     /** {@inheritDoc} */
238     // workaround for MPIR-140
239     // TODO Remove me when MSHARED-390 has been resolved
240     @Override
241     protected void startSection(String name) {
242         startSection(name, name);
243     }
244 
245     /**
246      * Start section with a name and a specific anchor.
247      *
248      * @param anchor not null
249      * @param name not null
250      */
251     // TODO Remove me when MSHARED-390 has been resolved
252     protected void startSection(String name, String anchor) {
253         section = section + 1;
254 
255         super.sink.anchor(HtmlTools.encodeId(anchor));
256         super.sink.anchor_();
257 
258         switch (section) {
259             case 1:
260                 sink.section1();
261                 sink.sectionTitle1();
262                 break;
263             case 2:
264                 sink.section2();
265                 sink.sectionTitle2();
266                 break;
267             case 3:
268                 sink.section3();
269                 sink.sectionTitle3();
270                 break;
271             case 4:
272                 sink.section4();
273                 sink.sectionTitle4();
274                 break;
275             case 5:
276                 sink.section5();
277                 sink.sectionTitle5();
278                 break;
279 
280             default:
281                 // TODO: warning - just don't start a section
282                 break;
283         }
284 
285         text(name);
286 
287         switch (section) {
288             case 1:
289                 sink.sectionTitle1_();
290                 break;
291             case 2:
292                 sink.sectionTitle2_();
293                 break;
294             case 3:
295                 sink.sectionTitle3_();
296                 break;
297             case 4:
298                 sink.sectionTitle4_();
299                 break;
300             case 5:
301                 sink.sectionTitle5_();
302                 break;
303 
304             default:
305                 // TODO: warning - just don't start a section
306                 break;
307         }
308     }
309 
310     /** {@inheritDoc} */
311     // workaround for MPIR-140
312     // TODO Remove me when MSHARED-390 has been resolved
313     @Override
314     protected void endSection() {
315         switch (section) {
316             case 1:
317                 sink.section1_();
318                 break;
319             case 2:
320                 sink.section2_();
321                 break;
322             case 3:
323                 sink.section3_();
324                 break;
325             case 4:
326                 sink.section4_();
327                 break;
328             case 5:
329                 sink.section5_();
330                 break;
331 
332             default:
333                 // TODO: warning - just don't start a section
334                 break;
335         }
336 
337         section = section - 1;
338 
339         if (section < 0) {
340             throw new IllegalStateException("Too many closing sections");
341         }
342     }
343 
344     // ----------------------------------------------------------------------
345     // Private methods
346     // ----------------------------------------------------------------------
347 
348     /**
349      * @param withClassifier <code>true</code> to include the classifier column, <code>false</code> otherwise.
350      * @param withOptional <code>true</code> to include the optional column, <code>false</code> otherwise.
351      * @return the dependency table header with/without classifier/optional column
352      * @see #renderArtifactRow(Artifact, boolean, boolean)
353      */
354     private String[] getDependencyTableHeader(boolean withClassifier, boolean withOptional) {
355         String groupId = getI18nString("column.groupId");
356         String artifactId = getI18nString("column.artifactId");
357         String version = getI18nString("column.version");
358         String classifier = getI18nString("column.classifier");
359         String type = getI18nString("column.type");
360         String license = getI18nString("column.licenses");
361         String optional = getI18nString("column.optional");
362 
363         if (withClassifier) {
364             if (withOptional) {
365                 return new String[] {groupId, artifactId, version, classifier, type, license, optional};
366             }
367 
368             return new String[] {groupId, artifactId, version, classifier, type, license};
369         }
370 
371         if (withOptional) {
372             return new String[] {groupId, artifactId, version, type, license, optional};
373         }
374 
375         return new String[] {groupId, artifactId, version, type, license};
376     }
377 
378     private void renderSectionProjectDependencies() {
379         startSection(getTitle());
380 
381         // collect dependencies by scope
382         Map<String, List<Artifact>> dependenciesByScope = dependencies.getDependenciesByScope(false);
383 
384         renderDependenciesForAllScopes(dependenciesByScope, false);
385 
386         endSection();
387     }
388 
389     /**
390      * @param dependenciesByScope map with supported scopes as key and a list of <code>Artifact</code> as values.
391      * @param isTransitive <code>true</code> if it is transitive dependencies rendering.
392      * @see Artifact#SCOPE_COMPILE
393      * @see Artifact#SCOPE_PROVIDED
394      * @see Artifact#SCOPE_RUNTIME
395      * @see Artifact#SCOPE_SYSTEM
396      * @see Artifact#SCOPE_TEST
397      */
398     private void renderDependenciesForAllScopes(Map<String, List<Artifact>> dependenciesByScope, boolean isTransitive) {
399         renderDependenciesForScope(
400                 Artifact.SCOPE_COMPILE, dependenciesByScope.get(Artifact.SCOPE_COMPILE), isTransitive);
401         renderDependenciesForScope(
402                 Artifact.SCOPE_RUNTIME, dependenciesByScope.get(Artifact.SCOPE_RUNTIME), isTransitive);
403         renderDependenciesForScope(Artifact.SCOPE_TEST, dependenciesByScope.get(Artifact.SCOPE_TEST), isTransitive);
404         renderDependenciesForScope(
405                 Artifact.SCOPE_PROVIDED, dependenciesByScope.get(Artifact.SCOPE_PROVIDED), isTransitive);
406         renderDependenciesForScope(Artifact.SCOPE_SYSTEM, dependenciesByScope.get(Artifact.SCOPE_SYSTEM), isTransitive);
407     }
408 
409     private void renderSectionProjectTransitiveDependencies() {
410         Map<String, List<Artifact>> dependenciesByScope = dependencies.getDependenciesByScope(true);
411 
412         startSection(getI18nString("transitive.title"));
413 
414         if (dependenciesByScope.values().isEmpty()) {
415             paragraph(getI18nString("transitive.nolist"));
416         } else {
417             paragraph(getI18nString("transitive.intro"));
418 
419             renderDependenciesForAllScopes(dependenciesByScope, true);
420         }
421 
422         endSection();
423     }
424 
425     private void renderSectionProjectDependencyGraph() {
426         startSection(getI18nString("graph.title"));
427 
428         // === SubSection: Dependency Tree
429         renderSectionDependencyTree();
430 
431         endSection();
432     }
433 
434     private void renderSectionDependencyTree() {
435         StringWriter sw = new StringWriter();
436         PrintWriter pw = new PrintWriter(sw);
437 
438         pw.println("      function toggleDependencyDetails( divId, imgId )");
439         pw.println("      {");
440         pw.println("        var div = document.getElementById( divId );");
441         pw.println("        var img = document.getElementById( imgId );");
442         pw.println("        if( div.style.display == '' )");
443         pw.println("        {");
444         pw.println("          div.style.display = 'none';");
445         pw.printf("          img.src='%s';%n", IMG_INFO_URL);
446         pw.printf("          img.alt='%s';%n", getI18nString("graph.icon.information"));
447         pw.println("        }");
448         pw.println("        else");
449         pw.println("        {");
450         pw.println("          div.style.display = '';");
451         pw.printf("          img.src='%s';%n", IMG_CLOSE_URL);
452         pw.printf("          img.alt='%s';%n", getI18nString("graph.icon.close"));
453         pw.println("        }");
454         pw.println("      }");
455 
456         javaScript(sw.toString());
457 
458         // for Dependencies Graph Tree
459         startSection(getI18nString("graph.tree.title"));
460 
461         sink.list();
462         printDependencyListing(dependencyNode);
463         sink.list_();
464 
465         endSection();
466     }
467 
468     private void renderSectionDependencyFileDetails() {
469         startSection(getI18nString("file.details.title"));
470 
471         List<Artifact> alldeps = dependencies.getAllDependencies();
472         Collections.sort(alldeps, getArtifactComparator());
473 
474         resolveAtrifacts(alldeps);
475 
476         // i18n
477         String filename = getI18nString("file.details.column.file");
478         String size = getI18nString("file.details.column.size");
479         String entries = getI18nString("file.details.column.entries");
480         String classes = getI18nString("file.details.column.classes");
481         String packages = getI18nString("file.details.column.packages");
482         String javaVersion = getI18nString("file.details.column.javaVersion");
483         String debugInformation = getI18nString("file.details.column.debuginformation");
484         String debugInformationTitle = getI18nString("file.details.columntitle.debuginformation");
485         String debugInformationCellYes = getI18nString("file.details.cell.debuginformation.yes");
486         String debugInformationCellNo = getI18nString("file.details.cell.debuginformation.no");
487         String aSealed = getI18nString("file.details.column.sealed");
488         String sealedCellYes = getI18nString("file.details.cell.sealed.yes");
489         String sealedCellNo = getI18nString("file.details.cell.sealed.no");
490 
491         int[] justification = new int[] {
492             Sink.JUSTIFY_LEFT,
493             Sink.JUSTIFY_RIGHT,
494             Sink.JUSTIFY_RIGHT,
495             Sink.JUSTIFY_RIGHT,
496             Sink.JUSTIFY_RIGHT,
497             Sink.JUSTIFY_CENTER,
498             Sink.JUSTIFY_CENTER,
499             Sink.JUSTIFY_CENTER
500         };
501 
502         startTable(justification, false);
503 
504         TotalCell totaldeps = new TotalCell();
505         TotalCell totaldepsize = new TotalCell(fileLengthDecimalFormat);
506         TotalCell totalentries = new TotalCell();
507         TotalCell totalclasses = new TotalCell();
508         TotalCell totalpackages = new TotalCell();
509         double highestTestJavaVersion = 0.0;
510         double highestNonTestJavaVersion = 0.0;
511         TotalCell totalDebugInformation = new TotalCell();
512         TotalCell totalsealed = new TotalCell();
513 
514         boolean hasSealed = hasSealed(alldeps);
515 
516         // Table header
517         String[] tableHeader;
518         String[] tableHeaderTitles;
519         if (hasSealed) {
520             tableHeader =
521                     new String[] {filename, size, entries, classes, packages, javaVersion, debugInformation, aSealed};
522             tableHeaderTitles = new String[] {null, null, null, null, null, null, debugInformationTitle, null};
523         } else {
524             tableHeader = new String[] {filename, size, entries, classes, packages, javaVersion, debugInformation};
525             tableHeaderTitles = new String[] {null, null, null, null, null, null, debugInformationTitle};
526         }
527         tableHeader(tableHeader, tableHeaderTitles);
528 
529         // Table rows
530         for (Artifact artifact : alldeps) {
531             if (artifact.getFile() == null) {
532                 log.warn("Artifact " + artifact.getId() + " has no file"
533                         + " and won't be listed in dependency files details.");
534                 continue;
535             }
536             if (SummaryTableRowOrder.fromScope(artifact.getScope()) == null) {
537                 log.warn("Artifact " + artifact.getId() + " has invalid scope"
538                         + " and won't be listed in dependency files details.");
539                 continue;
540             }
541 
542             File artifactFile = dependencies.getFile(artifact);
543 
544             totaldeps.incrementTotal(artifact.getScope());
545             totaldepsize.addTotal(artifactFile.length(), artifact.getScope());
546 
547             if (JAR_SUBTYPE.contains(artifact.getType().toLowerCase())) {
548                 try {
549                     JarData jarDetails = dependencies.getJarDependencyDetails(artifact);
550 
551                     totalentries.addTotal(jarDetails.getNumEntries(), artifact.getScope());
552                     totalclasses.addTotal(jarDetails.getNumClasses(), artifact.getScope());
553                     totalpackages.addTotal(jarDetails.getNumPackages(), artifact.getScope());
554 
555                     String jdkRevisionCellValue = jarDetails.getJdkRevision();
556                     String debugInformationCellValue = null;
557                     String sealedCellValue = null;
558 
559                     if (jdkRevisionCellValue != null) {
560                         try {
561                             double jdkRevision = Double.parseDouble(jdkRevisionCellValue);
562                             boolean isTestScope = Artifact.SCOPE_TEST.equalsIgnoreCase(artifact.getScope());
563                             if (isTestScope) {
564                                 highestTestJavaVersion = Math.max(highestTestJavaVersion, jdkRevision);
565                             } else {
566                                 highestNonTestJavaVersion = Math.max(highestNonTestJavaVersion, jdkRevision);
567                             }
568                         } catch (NumberFormatException e) {
569                             // ignore
570                         }
571 
572                         debugInformationCellValue = debugInformationCellNo;
573                         if (jarDetails.isDebugPresent()) {
574                             debugInformationCellValue = debugInformationCellYes;
575                             totalDebugInformation.incrementTotal(artifact.getScope());
576                         }
577 
578                         sealedCellValue = sealedCellNo;
579                         if (jarDetails.isSealed()) {
580                             sealedCellValue = sealedCellYes;
581                             totalsealed.incrementTotal(artifact.getScope());
582                         }
583                     }
584 
585                     String name = artifactFile.getName();
586                     String fileLength = fileLengthDecimalFormat.format(artifactFile.length());
587 
588                     if (artifactFile.isDirectory()) {
589                         File parent = artifactFile.getParentFile();
590                         name = parent.getParentFile().getName() + '/' + parent.getName() + '/' + artifactFile.getName();
591                         fileLength = "-";
592                     }
593 
594                     tableRow(hasSealed, new String[] {
595                         name,
596                         fileLength,
597                         String.valueOf(jarDetails.getNumEntries()),
598                         String.valueOf(jarDetails.getNumClasses()),
599                         String.valueOf(jarDetails.getNumPackages()),
600                         jdkRevisionCellValue,
601                         debugInformationCellValue,
602                         sealedCellValue
603                     });
604                 } catch (IOException e) {
605                     createExceptionInfoTableRow(artifact, artifactFile, e, hasSealed);
606                 }
607             } else {
608                 tableRow(hasSealed, new String[] {
609                     artifactFile.getName(),
610                     fileLengthDecimalFormat.format(artifactFile.length()),
611                     "",
612                     "",
613                     "",
614                     "",
615                     "",
616                     ""
617                 });
618             }
619         }
620 
621         // Total raws
622         tableHeader[0] = getI18nString("file.details.total");
623         tableHeader(tableHeader);
624 
625         justification[0] = Sink.JUSTIFY_RIGHT;
626         justification[6] = Sink.JUSTIFY_RIGHT;
627 
628         // calculate rowspan attr
629         int rowspan = computeRowspan(totaldeps);
630 
631         if (rowspan > 1) {
632             boolean insertRowspanAttr = false;
633             int column = 5; // Java Version's column
634             for (SummaryTableRowOrder currentRow : SummaryTableRowOrder.values()) {
635                 if (currentRow.getTotal(totaldeps) > 0) {
636                     int i = currentRow.ordinal();
637                     boolean alreadyInsertedRowspanAttr = insertRowspanAttr
638                             && (SummaryTableRowOrder.COMPILE_SCOPE.ordinal() < i
639                                     && i <= SummaryTableRowOrder.SYSTEM_SCOPE.ordinal());
640                     insertRowspanAttr = (SummaryTableRowOrder.COMPILE_SCOPE.ordinal() <= i
641                             && i <= SummaryTableRowOrder.SYSTEM_SCOPE.ordinal());
642                     justification[column] = (insertRowspanAttr && alreadyInsertedRowspanAttr)
643                             ? justification[column + 1]
644                             : Sink.JUSTIFY_CENTER;
645                     tableRowWithRowspan(
646                             hasSealed, insertRowspanAttr, alreadyInsertedRowspanAttr, column, rowspan, new String[] {
647                                 totaldeps.getTotalString(currentRow),
648                                 totaldepsize.getTotalString(currentRow),
649                                 totalentries.getTotalString(currentRow),
650                                 totalclasses.getTotalString(currentRow),
651                                 totalpackages.getTotalString(currentRow),
652                                 currentRow.formatMaxJavaVersionForScope(
653                                         javaVersionFormat, highestTestJavaVersion, highestNonTestJavaVersion),
654                                 totalDebugInformation.getTotalString(currentRow),
655                                 totalsealed.getTotalString(currentRow)
656                             });
657                 }
658             }
659         } else {
660             for (SummaryTableRowOrder currentRow : SummaryTableRowOrder.values()) {
661                 if (currentRow.getTotal(totaldeps) > 0) {
662                     tableRow(hasSealed, new String[] {
663                         totaldeps.getTotalString(currentRow),
664                         totaldepsize.getTotalString(currentRow),
665                         totalentries.getTotalString(currentRow),
666                         totalclasses.getTotalString(currentRow),
667                         totalpackages.getTotalString(currentRow),
668                         currentRow.formatMaxJavaVersionForScope(
669                                 javaVersionFormat, highestTestJavaVersion, highestNonTestJavaVersion),
670                         totalDebugInformation.getTotalString(currentRow),
671                         totalsealed.getTotalString(currentRow)
672                     });
673                 }
674             }
675         }
676 
677         endTable();
678         endSection();
679     }
680 
681     private int computeRowspan(TotalCell totaldeps) {
682         int rowspan = 0;
683         for (int i = SummaryTableRowOrder.COMPILE_SCOPE.ordinal();
684                 i <= SummaryTableRowOrder.SYSTEM_SCOPE.ordinal();
685                 i++) {
686             SummaryTableRowOrder currentRow = SummaryTableRowOrder.values()[i];
687             if (currentRow.getTotal(totaldeps) > 0) {
688                 rowspan++;
689             }
690         }
691         return rowspan;
692     }
693 
694     // Almost as same as in the abstract class but includes the title attribute
695     private void tableHeader(String[] content, String[] titles) {
696         sink.tableRow();
697 
698         if (content != null) {
699             if (titles != null && content.length != titles.length) {
700                 throw new IllegalArgumentException("Length of title array must equal the length of the content array");
701             }
702 
703             for (int i = 0; i < content.length; i++) {
704                 if (titles != null) {
705                     tableHeaderCell(content[i], titles[i]);
706                 } else {
707                     tableHeaderCell(content[i]);
708                 }
709             }
710         }
711 
712         sink.tableRow_();
713     }
714 
715     private void tableHeaderCell(String text, String title) {
716         if (title != null) {
717             sink.tableHeaderCell(new SinkEventAttributeSet(SinkEventAttributes.TITLE, title));
718         } else {
719             sink.tableHeaderCell();
720         }
721 
722         text(text);
723 
724         sink.tableHeaderCell_();
725     }
726 
727     private void tableRowWithRowspan(
728             boolean fullRow, boolean insert, boolean alreadyInserted, int contentIndex, int rowspan, String[] content) {
729         sink.tableRow();
730 
731         int count = fullRow ? content.length : (content.length - 1);
732 
733         for (int i = 0; i < count; i++) {
734             if (i == contentIndex && insert) {
735                 if (!alreadyInserted) {
736                     SinkEventAttributes att = new SinkEventAttributeSet();
737                     att.addAttribute(Attribute.ROWSPAN, rowspan);
738                     att.addAttribute(Attribute.STYLE, "vertical-align: middle;");
739                     sink.tableCell(att);
740                     text(content[i]);
741                     sink.tableCell_();
742                 }
743             } else {
744                 tableCell(content[i]);
745             }
746         }
747 
748         sink.tableRow_();
749     }
750 
751     private void tableRow(boolean fullRow, String[] content) {
752         sink.tableRow();
753 
754         int count = fullRow ? content.length : (content.length - 1);
755 
756         for (int i = 0; i < count; i++) {
757             tableCell(content[i]);
758         }
759 
760         sink.tableRow_();
761     }
762 
763     private void createExceptionInfoTableRow(Artifact artifact, File artifactFile, Exception e, boolean hasSealed) {
764         tableRow(
765                 hasSealed,
766                 new String[] {artifact.getId(), artifactFile.getAbsolutePath(), e.getMessage(), "", "", "", "", ""});
767     }
768 
769     private void renderSectionDependencyLicenseListing() {
770         startSection(getI18nString("graph.tables.licenses"));
771         printGroupedLicenses();
772         endSection();
773     }
774 
775     private void renderDependenciesForScope(String scope, List<Artifact> artifacts, boolean isTransitive) {
776         if (artifacts != null) {
777             boolean withClassifier = hasClassifier(artifacts);
778             boolean withOptional = hasOptional(artifacts);
779             String[] tableHeader = getDependencyTableHeader(withClassifier, withOptional);
780 
781             // can't use straight artifact comparison because we want optional last
782             Collections.sort(artifacts, getArtifactComparator());
783 
784             String anchorByScope = isTransitive
785                     ? getI18nString("transitive.title") + "_" + scope
786                     : getI18nString("title") + "_" + scope;
787             startSection(scope, anchorByScope);
788 
789             paragraph(getI18nString("intro." + scope));
790 
791             startTable();
792             tableHeader(tableHeader);
793             for (Artifact artifact : artifacts) {
794                 renderArtifactRow(artifact, withClassifier, withOptional);
795             }
796             endTable();
797 
798             endSection();
799         }
800     }
801 
802     private Comparator<Artifact> getArtifactComparator() {
803         return new Comparator<Artifact>() {
804             public int compare(Artifact a1, Artifact a2) {
805                 // put optional last
806                 if (a1.isOptional() && !a2.isOptional()) {
807                     return +1;
808                 } else if (!a1.isOptional() && a2.isOptional()) {
809                     return -1;
810                 } else {
811                     return a1.compareTo(a2);
812                 }
813             }
814         };
815     }
816 
817     /**
818      * @param artifact not null
819      * @param withClassifier <code>true</code> to include the classifier column, <code>false</code> otherwise.
820      * @param withOptional <code>true</code> to include the optional column, <code>false</code> otherwise.
821      * @see #getDependencyTableHeader(boolean, boolean)
822      */
823     private void renderArtifactRow(Artifact artifact, boolean withClassifier, boolean withOptional) {
824         String isOptional =
825                 artifact.isOptional() ? getI18nString("column.isOptional") : getI18nString("column.isNotOptional");
826 
827         String url = ProjectInfoReportUtils.getArtifactUrl(repositorySystem, artifact, projectBuilder, buildingRequest);
828         String artifactIdCell = ProjectInfoReportUtils.getArtifactIdCell(artifact.getArtifactId(), url);
829 
830         MavenProject artifactProject;
831         StringBuilder sb = new StringBuilder();
832         try {
833             artifactProject = repoUtils.getMavenProjectFromRepository(artifact);
834 
835             List<License> licenses = artifactProject.getLicenses();
836             for (License license : licenses) {
837                 String name = license.getName();
838                 if (licenseMappings != null && licenseMappings.containsKey(name)) {
839                     name = licenseMappings.get(name);
840                 }
841                 sb.append(ProjectInfoReportUtils.getArtifactIdCell(name, license.getUrl()));
842             }
843         } catch (ProjectBuildingException e) {
844             if (log.isDebugEnabled()) {
845                 log.debug("Unable to create Maven project from repository for artifact '" + artifact.getId() + "'", e);
846             } else {
847                 log.info("Unable to create Maven project from repository for artifact '" + artifact.getId()
848                         + "', for more information run with -X");
849             }
850         }
851 
852         String[] content;
853         if (withClassifier) {
854             content = new String[] {
855                 artifact.getGroupId(),
856                 artifactIdCell,
857                 artifact.getVersion(),
858                 artifact.getClassifier(),
859                 artifact.getType(),
860                 sb.toString(),
861                 isOptional
862             };
863         } else {
864             content = new String[] {
865                 artifact.getGroupId(),
866                 artifactIdCell,
867                 artifact.getVersion(),
868                 artifact.getType(),
869                 sb.toString(),
870                 isOptional
871             };
872         }
873 
874         tableRow(withOptional, content);
875     }
876 
877     private void printDependencyListing(DependencyNode node) {
878         Artifact artifact = node.getArtifact();
879         String id = artifact.getId();
880         String dependencyDetailId = "_dep" + idCounter++;
881         String imgId = "_img" + idCounter++;
882 
883         sink.listItem();
884 
885         sink.text(id + (StringUtils.isNotEmpty(artifact.getScope()) ? " (" + artifact.getScope() + ") " : " "));
886 
887         String javascript = String.format(
888                 "<img id=\"%s\" src=\"%s\" alt=\"%s\""
889                         + " onclick=\"toggleDependencyDetails( '%s', '%s' );\""
890                         + " style=\"cursor: pointer; vertical-align: text-bottom;\" />",
891                 imgId, IMG_INFO_URL, getI18nString("graph.icon.information"), dependencyDetailId, imgId);
892 
893         sink.rawText(javascript);
894 
895         printDescriptionsAndURLs(node, dependencyDetailId);
896 
897         if (!node.getChildren().isEmpty()) {
898             boolean toBeIncluded = false;
899             List<DependencyNode> subList = new ArrayList<>();
900             for (DependencyNode dep : node.getChildren()) {
901                 if (dependencies.getAllDependencies().contains(dep.getArtifact())) {
902                     subList.add(dep);
903                     toBeIncluded = true;
904                 }
905             }
906 
907             if (toBeIncluded) {
908                 sink.list();
909                 for (DependencyNode dep : subList) {
910                     printDependencyListing(dep);
911                 }
912                 sink.list_();
913             }
914         }
915 
916         sink.listItem_();
917     }
918 
919     private void printDescriptionsAndURLs(DependencyNode node, String uid) {
920         Artifact artifact = node.getArtifact();
921         String id = artifact.getId();
922         String unknownLicenseMessage = getI18nString("graph.tables.unknown");
923 
924         sink.rawText("<div id=\"" + uid + "\" style=\"display:none\">");
925 
926         if (!Artifact.SCOPE_SYSTEM.equals(artifact.getScope())) {
927             try {
928                 MavenProject artifactProject = repoUtils.getMavenProjectFromRepository(artifact);
929                 String artifactDescription = artifactProject.getDescription();
930                 String artifactUrl = artifactProject.getUrl();
931                 String artifactName = artifactProject.getName();
932 
933                 List<License> licenses = artifactProject.getLicenses();
934 
935                 startTable();
936 
937                 sink.tableRow();
938                 sink.tableHeaderCell();
939                 sink.text(artifactName);
940                 sink.tableHeaderCell_();
941                 sink.tableRow_();
942 
943                 sink.tableRow();
944                 sink.tableCell();
945 
946                 sink.paragraph();
947                 sink.bold();
948                 sink.text(getI18nString("column.description") + ": ");
949                 sink.bold_();
950                 if (artifactDescription != null && !artifactDescription.isEmpty()) {
951                     sink.text(artifactDescription);
952                 } else {
953                     sink.text(getI18nString("index", "nodescription"));
954                 }
955                 sink.paragraph_();
956 
957                 if (artifactUrl != null && !artifactUrl.isEmpty()) {
958                     sink.paragraph();
959                     sink.bold();
960                     sink.text(getI18nString("column.url") + ": ");
961                     sink.bold_();
962                     if (ProjectInfoReportUtils.isArtifactUrlValid(artifactUrl)) {
963                         sink.link(artifactUrl);
964                         sink.text(artifactUrl);
965                         sink.link_();
966                     } else {
967                         sink.text(artifactUrl);
968                     }
969                     sink.paragraph_();
970                 }
971 
972                 sink.paragraph();
973                 sink.bold();
974                 sink.text(getI18nString("licenses", "title") + ": ");
975                 sink.bold_();
976                 if (!licenses.isEmpty()) {
977 
978                     for (Iterator<License> it = licenses.iterator(); it.hasNext(); ) {
979                         License license = it.next();
980 
981                         String licenseName = license.getName();
982                         if (licenseMappings != null && licenseMappings.containsKey(licenseName)) {
983                             licenseName = licenseMappings.get(licenseName);
984                         }
985                         if (licenseName == null || licenseName.isEmpty()) {
986                             licenseName = getI18nString("unnamed");
987                         }
988 
989                         String licenseUrl = license.getUrl();
990 
991                         if (licenseUrl != null) {
992                             sink.link(licenseUrl);
993                         }
994                         sink.text(licenseName);
995 
996                         if (licenseUrl != null) {
997                             sink.link_();
998                         }
999 
1000                         if (it.hasNext()) {
1001                             sink.text(", ");
1002                         }
1003 
1004                         licenseMap.put(licenseName, artifactName);
1005                     }
1006                 } else {
1007                     sink.text(getI18nString("licenses", "nolicense"));
1008 
1009                     licenseMap.put(unknownLicenseMessage, artifactName);
1010                 }
1011                 sink.paragraph_();
1012 
1013                 sink.tableCell_();
1014                 sink.tableRow_();
1015 
1016                 endTable();
1017             } catch (ProjectBuildingException e) {
1018                 sink.text(getI18nString("index", "nodescription"));
1019                 if (log.isDebugEnabled()) {
1020                     log.debug(
1021                             "Unable to create Maven project from repository for artifact '" + artifact.getId() + "'",
1022                             e);
1023                 } else {
1024                     log.info("Unable to create Maven project from repository for artifact '" + artifact.getId()
1025                             + "', for more information run with -X");
1026                 }
1027             }
1028         } else {
1029             startTable();
1030 
1031             sink.tableRow();
1032             sink.tableHeaderCell();
1033             sink.text(id);
1034             sink.tableHeaderCell_();
1035             sink.tableRow_();
1036 
1037             sink.tableRow();
1038             sink.tableCell();
1039 
1040             sink.paragraph();
1041             sink.bold();
1042             sink.text(getI18nString("column.description") + ": ");
1043             sink.bold_();
1044             sink.text(getI18nString("index", "nodescription"));
1045             sink.paragraph_();
1046 
1047             if (artifact.getFile() != null) {
1048                 sink.paragraph();
1049                 sink.bold();
1050                 sink.text(getI18nString("column.url") + ": ");
1051                 sink.bold_();
1052                 sink.text(artifact.getFile().getAbsolutePath());
1053                 sink.paragraph_();
1054             }
1055 
1056             sink.tableCell_();
1057             sink.tableRow_();
1058 
1059             endTable();
1060         }
1061 
1062         sink.rawText("</div>");
1063     }
1064 
1065     private void printGroupedLicenses() {
1066         for (Map.Entry<String, Object> entry : licenseMap.entrySet()) {
1067             String licenseName = entry.getKey();
1068             if (licenseName == null || licenseName.isEmpty()) {
1069                 licenseName = getI18nString("unnamed");
1070             }
1071 
1072             sink.paragraph();
1073             sink.bold();
1074             sink.text(licenseName);
1075             sink.text(": ");
1076             sink.bold_();
1077 
1078             @SuppressWarnings("unchecked")
1079             SortedSet<String> projects = (SortedSet<String>) entry.getValue();
1080 
1081             for (Iterator<String> iterator = projects.iterator(); iterator.hasNext(); ) {
1082                 String projectName = iterator.next();
1083                 sink.text(projectName);
1084                 if (iterator.hasNext()) {
1085                     sink.text(", ");
1086                 }
1087             }
1088 
1089             sink.paragraph_();
1090         }
1091     }
1092 
1093     /**
1094      * Resolves all given artifacts with {@link RepositoryUtils}.
1095      *
1096      ** @param artifacts not null
1097      */
1098     private void resolveAtrifacts(List<Artifact> artifacts) {
1099         for (Artifact artifact : artifacts) {
1100             // TODO site:run Why do we need to resolve this...
1101             if (artifact.getFile() == null) {
1102                 if (Artifact.SCOPE_SYSTEM.equals(artifact.getScope())) {
1103                     // can not resolve system scope artifact file
1104                     continue;
1105                 }
1106 
1107                 try {
1108                     repoUtils.resolve(artifact);
1109                 } catch (ArtifactResolverException e) {
1110                     log.error("Artifact " + artifact.getId() + " can't be resolved.", e);
1111                     continue;
1112                 }
1113 
1114                 if (artifact.getFile() == null) {
1115                     log.error("Artifact " + artifact.getId() + " has no file, even after resolution.");
1116                 }
1117             }
1118         }
1119     }
1120 
1121     /**
1122      * @param artifacts not null
1123      * @return <code>true</code> if one artifact in the list has a classifier, <code>false</code> otherwise.
1124      */
1125     private boolean hasClassifier(List<Artifact> artifacts) {
1126         for (Artifact artifact : artifacts) {
1127             if (StringUtils.isNotEmpty(artifact.getClassifier())) {
1128                 return true;
1129             }
1130         }
1131 
1132         return false;
1133     }
1134 
1135     /**
1136      * @param artifacts not null
1137      * @return <code>true</code> if one artifact in the list is optional, <code>false</code> otherwise.
1138      */
1139     private boolean hasOptional(List<Artifact> artifacts) {
1140         for (Artifact artifact : artifacts) {
1141             if (artifact.isOptional()) {
1142                 return true;
1143             }
1144         }
1145 
1146         return false;
1147     }
1148 
1149     /**
1150      * @param artifacts not null
1151      * @return <code>true</code> if one artifact in the list is sealed, <code>false</code> otherwise.
1152      */
1153     private boolean hasSealed(List<Artifact> artifacts) {
1154         for (Artifact artifact : artifacts) {
1155             if (artifact.getFile() != null
1156                     && JAR_SUBTYPE.contains(artifact.getType().toLowerCase())) {
1157                 try {
1158                     JarData jarDetails = dependencies.getJarDependencyDetails(artifact);
1159                     if (jarDetails.isSealed()) {
1160                         return true;
1161                     }
1162                 } catch (IOException e) {
1163                     log.error("Artifact " + artifact.getId() + " caused IOException: " + e.getMessage(), e);
1164                 }
1165             }
1166         }
1167         return false;
1168     }
1169 
1170     // CHECKSTYLE_OFF: LineLength
1171     /**
1172      * Formats file length with the associated <a href="https://en.wikipedia.org/wiki/Metric_prefix">SI</a> prefix
1173      * (GB, MB, kB) and using the pattern <code>###0.#</code> by default.
1174      *
1175      * @see <a href="https://en.wikipedia.org/wiki/Metric_prefix">https://en.wikipedia.org/wiki/Metric_prefix</a>
1176      * @see <a href="https://en.wikipedia.org/wiki/Binary_prefix">https://en.wikipedia.org/wiki/Binary_prefix</a>
1177      * @see <a
1178      *      href="https://en.wikipedia.org/wiki/Octet_%28computing%29">https://en.wikipedia.org/wiki/Octet_(computing)</a>
1179      */
1180     // CHECKSTYLE_ON: LineLength
1181     static class FileDecimalFormat extends DecimalFormat {
1182         private static final long serialVersionUID = 4062503546523610081L;
1183 
1184         private final I18N i18n;
1185 
1186         private final Locale locale;
1187 
1188         /**
1189          * Default constructor
1190          *
1191          * @param i18n
1192          * @param locale
1193          */
1194         FileDecimalFormat(I18N i18n, Locale locale) {
1195             super("###0.#");
1196 
1197             this.i18n = i18n;
1198             this.locale = locale;
1199         }
1200 
1201         /** {@inheritDoc} */
1202         @Override
1203         public StringBuffer format(long fs, StringBuffer result, FieldPosition fieldPosition) {
1204             if (fs > 1000 * 1000 * 1000) {
1205                 result = super.format((float) fs / (1000 * 1000 * 1000), result, fieldPosition);
1206                 result.append(" ").append(getString("report.dependencies.file.details.column.size.gb"));
1207                 return result;
1208             }
1209 
1210             if (fs > 1000 * 1000) {
1211                 result = super.format((float) fs / (1000 * 1000), result, fieldPosition);
1212                 result.append(" ").append(getString("report.dependencies.file.details.column.size.mb"));
1213                 return result;
1214             }
1215 
1216             result = super.format((float) fs / 1000, result, fieldPosition);
1217             result.append(" ").append(getString("report.dependencies.file.details.column.size.kb"));
1218             return result;
1219         }
1220 
1221         private String getString(String key) {
1222             return i18n.getString("project-info-reports", locale, key);
1223         }
1224     }
1225 
1226     /**
1227      * Combine total and total by scope in a cell.
1228      */
1229     static class TotalCell {
1230         public enum SummaryTableRowOrder {
1231             // Do not change the physical order of these values
1232             TOTALS {
1233                 @Override
1234                 public void addTotal(TotalCell cell, long value) {
1235                     cell.total += value;
1236                 }
1237 
1238                 @Override
1239                 public long getTotal(TotalCell cell) {
1240                     return cell.total;
1241                 }
1242 
1243                 @Override
1244                 protected String formatMaxJavaVersionForScope(
1245                         MessageFormat javaVersionFormat,
1246                         double highestTestJavaVersion,
1247                         double highestNonTestJavaVersion) {
1248                     double highestJavaVersion = Math.max(highestTestJavaVersion, highestNonTestJavaVersion);
1249                     return javaVersionFormat.format(new Object[] {highestJavaVersion});
1250                 }
1251             },
1252             COMPILE_SCOPE(Artifact.SCOPE_COMPILE) {
1253                 @Override
1254                 public void addTotal(TotalCell cell, long value) {
1255                     cell.totalCompileScope += value;
1256                 }
1257 
1258                 @Override
1259                 public long getTotal(TotalCell cell) {
1260                     return cell.totalCompileScope;
1261                 }
1262             },
1263             RUNTIME_SCOPE(Artifact.SCOPE_RUNTIME) {
1264                 @Override
1265                 public void addTotal(TotalCell cell, long value) {
1266                     cell.totalRuntimeScope += value;
1267                 }
1268 
1269                 @Override
1270                 public long getTotal(TotalCell cell) {
1271                     return cell.totalRuntimeScope;
1272                 }
1273             },
1274             PROVIDED_SCOPE(Artifact.SCOPE_PROVIDED) {
1275                 @Override
1276                 public void addTotal(TotalCell cell, long value) {
1277                     cell.totalProvidedScope += value;
1278                 }
1279 
1280                 @Override
1281                 public long getTotal(TotalCell cell) {
1282                     return cell.totalProvidedScope;
1283                 }
1284             },
1285             SYSTEM_SCOPE(Artifact.SCOPE_SYSTEM) {
1286                 @Override
1287                 public void addTotal(TotalCell cell, long value) {
1288                     cell.totalSystemScope += value;
1289                 }
1290 
1291                 @Override
1292                 public long getTotal(TotalCell cell) {
1293                     return cell.totalSystemScope;
1294                 }
1295             },
1296             TEST_SCOPE(Artifact.SCOPE_TEST) {
1297                 @Override
1298                 public void addTotal(TotalCell cell, long value) {
1299                     cell.totalTestScope += value;
1300                 }
1301 
1302                 @Override
1303                 public long getTotal(TotalCell cell) {
1304                     return cell.totalTestScope;
1305                 }
1306 
1307                 @Override
1308                 protected String formatMaxJavaVersionForScope(
1309                         MessageFormat javaVersionFormat,
1310                         double highestTestJavaVersion,
1311                         double highestNonTestJavaVersion) {
1312                     return javaVersionFormat.format(new Object[] {highestTestJavaVersion});
1313                 }
1314             };
1315 
1316             private static final Map<String, SummaryTableRowOrder> MAP_BY_SCOPE = new HashMap<>();
1317 
1318             static {
1319                 // scope string => enum mapping
1320                 for (SummaryTableRowOrder e : SummaryTableRowOrder.values()) {
1321                     MAP_BY_SCOPE.put(e.getScope(), e);
1322                 }
1323             }
1324 
1325             public static SummaryTableRowOrder fromScope(String scope) {
1326                 return MAP_BY_SCOPE.get(scope);
1327             }
1328 
1329             private String scope;
1330 
1331             SummaryTableRowOrder() {
1332                 this(null);
1333             }
1334 
1335             SummaryTableRowOrder(String scope) {
1336                 this.scope = scope;
1337             }
1338 
1339             public String getScope() {
1340                 return this.scope;
1341             }
1342 
1343             protected String formatMaxJavaVersionForScope(
1344                     MessageFormat javaVersionFormat, double highestTestJavaVersion, double highestNonTestJavaVersion) {
1345                 return javaVersionFormat.format(new Object[] {highestNonTestJavaVersion});
1346             }
1347 
1348             public abstract void addTotal(TotalCell cell, long value);
1349 
1350             public abstract long getTotal(TotalCell cell);
1351         }
1352 
1353         DecimalFormat decimalFormat;
1354 
1355         long total = 0;
1356 
1357         long totalCompileScope = 0;
1358 
1359         long totalTestScope = 0;
1360 
1361         long totalRuntimeScope = 0;
1362 
1363         long totalProvidedScope = 0;
1364 
1365         long totalSystemScope = 0;
1366 
1367         TotalCell() {}
1368 
1369         TotalCell(DecimalFormat decimalFormat) {
1370             this.decimalFormat = decimalFormat;
1371         }
1372 
1373         void incrementTotal(String scope) {
1374             addTotal(1, scope);
1375         }
1376 
1377         String getTotalString(SummaryTableRowOrder currentRow) {
1378             long totalString = currentRow.getTotal(this);
1379 
1380             if (totalString <= 0) {
1381                 return "";
1382             }
1383 
1384             StringBuilder sb = new StringBuilder();
1385             if (currentRow.compareTo(SummaryTableRowOrder.COMPILE_SCOPE) >= 0) {
1386                 sb.append(currentRow.getScope()).append(": ");
1387             }
1388             if (decimalFormat != null) {
1389                 sb.append(decimalFormat.format(currentRow.getTotal(this)));
1390             } else {
1391                 sb.append(currentRow.getTotal(this));
1392             }
1393 
1394             return sb.toString();
1395         }
1396 
1397         void addTotal(long add, String scope) {
1398             SummaryTableRowOrder.TOTALS.addTotal(this, add);
1399             SummaryTableRowOrder currentRow = SummaryTableRowOrder.fromScope(scope);
1400             currentRow.addTotal(this, add);
1401         }
1402 
1403         /** {@inheritDoc} */
1404         public String toString() {
1405             StringBuilder sb = new StringBuilder();
1406             if (decimalFormat != null) {
1407                 sb.append(decimalFormat.format(total));
1408             } else {
1409                 sb.append(total);
1410             }
1411 
1412             sb.append(" (");
1413 
1414             boolean needSeparator = false;
1415             for (int i = SummaryTableRowOrder.COMPILE_SCOPE.ordinal();
1416                     i < SummaryTableRowOrder.TEST_SCOPE.ordinal();
1417                     i++) {
1418                 SummaryTableRowOrder currentRow = SummaryTableRowOrder.values()[i];
1419                 if (currentRow.getTotal(this) > 0) {
1420                     if (needSeparator) {
1421                         sb.append(", ");
1422                     }
1423                     sb.append(getTotalString(currentRow));
1424                     needSeparator = true;
1425                 }
1426             }
1427 
1428             sb.append(")");
1429 
1430             return sb.toString();
1431         }
1432     }
1433 }