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