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