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 jarData = dependencies.getJarDependencyDetails(artifact);
554 
555                     totalentries.addTotal(jarData.getNumEntries(), artifact.getScope());
556                     totalclasses.addTotal(jarData.getNumClasses(), artifact.getScope());
557                     totalpackages.addTotal(jarData.getNumPackages(), artifact.getScope());
558 
559                     String jdkRevisionCellValue = jarData.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 (jarData.isDebugPresent()) {
578                             debugInformationCellValue = debugInformationCellYes;
579                             totalDebugInformation.incrementTotal(artifact.getScope());
580                         }
581 
582                         sealedCellValue = sealedCellNo;
583                         if (jarData.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 (jarData.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, fileLength, String.valueOf(jarData.getNumEntries()), "", "", "", "", sealedCellValue
606                         });
607 
608                         JarVersionedRuntimes versionedRuntimes = jarData.getVersionedRuntimes();
609                         Collection<JarVersionedRuntime> versionedRuntimeList =
610                                 versionedRuntimes.getVersionedRuntimeMap().values();
611 
612                         // root content information row
613                         tableRow(hasSealed, new String[] {
614                             rootTag,
615                             "",
616                             String.valueOf(jarData.getNumRootEntries()),
617                             String.valueOf(jarData.getNumClasses()),
618                             String.valueOf(jarData.getNumPackages()),
619                             jdkRevisionCellValue,
620                             debugInformationCellValue,
621                             ""
622                         });
623 
624                         for (JarVersionedRuntime versionedRuntime : versionedRuntimeList) {
625                             JarClasses versionedJarClasses = versionedRuntime.getJarClasses();
626 
627                             debugInformationCellValue = versionedJarClasses.isDebugPresent()
628                                     ? debugInformationCellYes
629                                     : debugInformationCellNo;
630 
631                             tableRow(hasSealed, new String[] {
632                                 versionedTag,
633                                 "",
634                                 String.valueOf(versionedRuntime.getNumEntries()),
635                                 String.valueOf(
636                                         versionedJarClasses.getClassNames().size()),
637                                 String.valueOf(versionedJarClasses.getPackages().size()),
638                                 versionedJarClasses.getJdkRevision(),
639                                 debugInformationCellValue,
640                                 ""
641                             });
642                         }
643                     } else {
644                         tableRow(hasSealed, new String[] {
645                             name,
646                             fileLength,
647                             String.valueOf(jarData.getNumEntries()),
648                             String.valueOf(jarData.getNumClasses()),
649                             String.valueOf(jarData.getNumPackages()),
650                             jdkRevisionCellValue,
651                             debugInformationCellValue,
652                             sealedCellValue
653                         });
654                     }
655                 } catch (IOException e) {
656                     createExceptionInfoTableRow(artifact, artifactFile, e, hasSealed);
657                 }
658             } else {
659                 tableRow(hasSealed, new String[] {
660                     artifactFile.getName(),
661                     fileLengthDecimalFormat.format(artifactFile.length()),
662                     "",
663                     "",
664                     "",
665                     "",
666                     "",
667                     ""
668                 });
669             }
670         }
671 
672         // Total raws
673         tableHeader[0] = getI18nString("file.details.total");
674         tableHeader(tableHeader);
675 
676         justification[0] = Sink.JUSTIFY_RIGHT;
677         justification[6] = Sink.JUSTIFY_RIGHT;
678 
679         // calculate rowspan attr
680         int rowspan = computeRowspan(totaldeps);
681 
682         if (rowspan > 1) {
683             boolean insertRowspanAttr = false;
684             int column = 5; // Java Version's column
685             for (SummaryTableRowOrder currentRow : SummaryTableRowOrder.values()) {
686                 if (currentRow.getTotal(totaldeps) > 0) {
687                     int i = currentRow.ordinal();
688                     boolean alreadyInsertedRowspanAttr = insertRowspanAttr
689                             && (SummaryTableRowOrder.COMPILE_SCOPE.ordinal() < i
690                                     && i <= SummaryTableRowOrder.SYSTEM_SCOPE.ordinal());
691                     insertRowspanAttr = (SummaryTableRowOrder.COMPILE_SCOPE.ordinal() <= i
692                             && i <= SummaryTableRowOrder.SYSTEM_SCOPE.ordinal());
693                     justification[column] = (insertRowspanAttr && alreadyInsertedRowspanAttr)
694                             ? justification[column + 1]
695                             : Sink.JUSTIFY_CENTER;
696                     tableRowWithRowspan(
697                             hasSealed, insertRowspanAttr, alreadyInsertedRowspanAttr, column, rowspan, new String[] {
698                                 totaldeps.getTotalString(currentRow),
699                                 totaldepsize.getTotalString(currentRow),
700                                 totalentries.getTotalString(currentRow),
701                                 totalclasses.getTotalString(currentRow),
702                                 totalpackages.getTotalString(currentRow),
703                                 currentRow.formatMaxJavaVersionForScope(
704                                         javaVersionFormat, highestTestJavaVersion, highestNonTestJavaVersion),
705                                 totalDebugInformation.getTotalString(currentRow),
706                                 totalsealed.getTotalString(currentRow)
707                             });
708                 }
709             }
710         } else {
711             for (SummaryTableRowOrder currentRow : SummaryTableRowOrder.values()) {
712                 if (currentRow.getTotal(totaldeps) > 0) {
713                     tableRow(hasSealed, new String[] {
714                         totaldeps.getTotalString(currentRow),
715                         totaldepsize.getTotalString(currentRow),
716                         totalentries.getTotalString(currentRow),
717                         totalclasses.getTotalString(currentRow),
718                         totalpackages.getTotalString(currentRow),
719                         currentRow.formatMaxJavaVersionForScope(
720                                 javaVersionFormat, highestTestJavaVersion, highestNonTestJavaVersion),
721                         totalDebugInformation.getTotalString(currentRow),
722                         totalsealed.getTotalString(currentRow)
723                     });
724                 }
725             }
726         }
727 
728         endTable();
729         endSection();
730     }
731 
732     private int computeRowspan(TotalCell totaldeps) {
733         int rowspan = 0;
734         for (int i = SummaryTableRowOrder.COMPILE_SCOPE.ordinal();
735                 i <= SummaryTableRowOrder.SYSTEM_SCOPE.ordinal();
736                 i++) {
737             SummaryTableRowOrder currentRow = SummaryTableRowOrder.values()[i];
738             if (currentRow.getTotal(totaldeps) > 0) {
739                 rowspan++;
740             }
741         }
742         return rowspan;
743     }
744 
745     // Almost as same as in the abstract class but includes the title attribute
746     private void tableHeader(String[] content, String[] titles) {
747         sink.tableRow();
748 
749         if (content != null) {
750             if (titles != null && content.length != titles.length) {
751                 throw new IllegalArgumentException("Length of title array must equal the length of the content array");
752             }
753 
754             for (int i = 0; i < content.length; i++) {
755                 if (titles != null) {
756                     tableHeaderCell(content[i], titles[i]);
757                 } else {
758                     tableHeaderCell(content[i]);
759                 }
760             }
761         }
762 
763         sink.tableRow_();
764     }
765 
766     private void tableHeaderCell(String text, String title) {
767         if (title != null) {
768             sink.tableHeaderCell(new SinkEventAttributeSet(SinkEventAttributes.TITLE, title));
769         } else {
770             sink.tableHeaderCell();
771         }
772 
773         text(text);
774 
775         sink.tableHeaderCell_();
776     }
777 
778     private void tableRowWithRowspan(
779             boolean fullRow, boolean insert, boolean alreadyInserted, int contentIndex, int rowspan, String[] content) {
780         sink.tableRow();
781 
782         int count = fullRow ? content.length : (content.length - 1);
783 
784         for (int i = 0; i < count; i++) {
785             if (i == contentIndex && insert) {
786                 if (!alreadyInserted) {
787                     SinkEventAttributes att = new SinkEventAttributeSet();
788                     att.addAttribute(Attribute.ROWSPAN, rowspan);
789                     att.addAttribute(Attribute.STYLE, "vertical-align: middle;");
790                     sink.tableCell(att);
791                     text(content[i]);
792                     sink.tableCell_();
793                 }
794             } else {
795                 tableCell(content[i]);
796             }
797         }
798 
799         sink.tableRow_();
800     }
801 
802     private void tableRow(boolean fullRow, String[] content) {
803         sink.tableRow();
804 
805         int count = fullRow ? content.length : (content.length - 1);
806 
807         for (int i = 0; i < count; i++) {
808             tableCell(content[i]);
809         }
810 
811         sink.tableRow_();
812     }
813 
814     private void createExceptionInfoTableRow(Artifact artifact, File artifactFile, Exception e, boolean hasSealed) {
815         tableRow(
816                 hasSealed,
817                 new String[] {artifact.getId(), artifactFile.getAbsolutePath(), e.getMessage(), "", "", "", "", ""});
818     }
819 
820     private void renderSectionDependencyLicenseListing() {
821         startSection(getI18nString("graph.tables.licenses"));
822         printGroupedLicenses();
823         endSection();
824     }
825 
826     private void renderDependenciesForScope(String scope, List<Artifact> artifacts, boolean isTransitive) {
827         if (artifacts != null) {
828             boolean withClassifier = hasClassifier(artifacts);
829             boolean withOptional = hasOptional(artifacts);
830             String[] tableHeader = getDependencyTableHeader(withClassifier, withOptional);
831 
832             // can't use straight artifact comparison because we want optional last
833             Collections.sort(artifacts, getArtifactComparator());
834 
835             String anchorByScope = isTransitive
836                     ? getI18nString("transitive.title") + "_" + scope
837                     : getI18nString("title") + "_" + scope;
838             startSection(scope, anchorByScope);
839 
840             paragraph(getI18nString("intro." + scope));
841 
842             startTable();
843             tableHeader(tableHeader);
844             for (Artifact artifact : artifacts) {
845                 renderArtifactRow(artifact, withClassifier, withOptional);
846             }
847             endTable();
848 
849             endSection();
850         }
851     }
852 
853     private Comparator<Artifact> getArtifactComparator() {
854         return new Comparator<Artifact>() {
855             public int compare(Artifact a1, Artifact a2) {
856                 // put optional last
857                 if (a1.isOptional() && !a2.isOptional()) {
858                     return +1;
859                 } else if (!a1.isOptional() && a2.isOptional()) {
860                     return -1;
861                 } else {
862                     return a1.compareTo(a2);
863                 }
864             }
865         };
866     }
867 
868     /**
869      * @param artifact not null
870      * @param withClassifier <code>true</code> to include the classifier column, <code>false</code> otherwise.
871      * @param withOptional <code>true</code> to include the optional column, <code>false</code> otherwise.
872      * @see #getDependencyTableHeader(boolean, boolean)
873      */
874     private void renderArtifactRow(Artifact artifact, boolean withClassifier, boolean withOptional) {
875         String isOptional =
876                 artifact.isOptional() ? getI18nString("column.isOptional") : getI18nString("column.isNotOptional");
877 
878         String url = ProjectInfoReportUtils.getArtifactUrl(repositorySystem, artifact, projectBuilder, buildingRequest);
879         String artifactIdCell = ProjectInfoReportUtils.getArtifactIdCell(artifact.getArtifactId(), url);
880 
881         MavenProject artifactProject;
882         StringBuilder sb = new StringBuilder();
883         try {
884             artifactProject = repoUtils.getMavenProjectFromRepository(artifact);
885 
886             List<License> licenses = artifactProject.getLicenses();
887             for (License license : licenses) {
888                 String name = license.getName();
889                 if (licenseMappings != null && licenseMappings.containsKey(name)) {
890                     name = licenseMappings.get(name);
891                 }
892                 sb.append(ProjectInfoReportUtils.getArtifactIdCell(name, license.getUrl()));
893             }
894         } catch (ProjectBuildingException e) {
895             if (log.isDebugEnabled()) {
896                 log.debug("Unable to create Maven project from repository for artifact '" + artifact.getId() + "'", e);
897             } else {
898                 log.info("Unable to create Maven project from repository for artifact '" + artifact.getId()
899                         + "', for more information run with -X");
900             }
901         }
902 
903         String[] content;
904         if (withClassifier) {
905             content = new String[] {
906                 artifact.getGroupId(),
907                 artifactIdCell,
908                 artifact.getVersion(),
909                 artifact.getClassifier(),
910                 artifact.getType(),
911                 sb.toString(),
912                 isOptional
913             };
914         } else {
915             content = new String[] {
916                 artifact.getGroupId(),
917                 artifactIdCell,
918                 artifact.getVersion(),
919                 artifact.getType(),
920                 sb.toString(),
921                 isOptional
922             };
923         }
924 
925         tableRow(withOptional, content);
926     }
927 
928     private void printDependencyListing(DependencyNode node) {
929         Artifact artifact = node.getArtifact();
930         String id = artifact.getId();
931         String dependencyDetailId = "_dep" + idCounter++;
932         String imgId = "_img" + idCounter++;
933 
934         sink.listItem();
935 
936         sink.text(id + (StringUtils.isNotEmpty(artifact.getScope()) ? " (" + artifact.getScope() + ") " : " "));
937 
938         String javascript = String.format(
939                 "<img id=\"%s\" src=\"%s\" alt=\"%s\""
940                         + " onclick=\"toggleDependencyDetails( '%s', '%s' );\""
941                         + " style=\"cursor: pointer; vertical-align: text-bottom;\" />",
942                 imgId, IMG_INFO_URL, getI18nString("graph.icon.information"), dependencyDetailId, imgId);
943 
944         sink.rawText(javascript);
945 
946         printDescriptionsAndURLs(node, dependencyDetailId);
947 
948         if (!node.getChildren().isEmpty()) {
949             boolean toBeIncluded = false;
950             List<DependencyNode> subList = new ArrayList<>();
951             for (DependencyNode dep : node.getChildren()) {
952                 if (dependencies.getAllDependencies().contains(dep.getArtifact())) {
953                     subList.add(dep);
954                     toBeIncluded = true;
955                 }
956             }
957 
958             if (toBeIncluded) {
959                 sink.list();
960                 for (DependencyNode dep : subList) {
961                     printDependencyListing(dep);
962                 }
963                 sink.list_();
964             }
965         }
966 
967         sink.listItem_();
968     }
969 
970     private void printDescriptionsAndURLs(DependencyNode node, String uid) {
971         Artifact artifact = node.getArtifact();
972         String id = artifact.getId();
973         String unknownLicenseMessage = getI18nString("graph.tables.unknown");
974 
975         sink.rawText("<div id=\"" + uid + "\" style=\"display:none\">");
976 
977         if (!Artifact.SCOPE_SYSTEM.equals(artifact.getScope())) {
978             try {
979                 MavenProject artifactProject = repoUtils.getMavenProjectFromRepository(artifact);
980                 String artifactDescription = artifactProject.getDescription();
981                 String artifactUrl = artifactProject.getUrl();
982                 String artifactName = artifactProject.getName();
983 
984                 List<License> licenses = artifactProject.getLicenses();
985 
986                 startTable();
987 
988                 sink.tableRow();
989                 sink.tableHeaderCell();
990                 sink.text(artifactName);
991                 sink.tableHeaderCell_();
992                 sink.tableRow_();
993 
994                 sink.tableRow();
995                 sink.tableCell();
996 
997                 sink.paragraph();
998                 sink.bold();
999                 sink.text(getI18nString("column.description") + ": ");
1000                 sink.bold_();
1001                 if (artifactDescription != null && !artifactDescription.isEmpty()) {
1002                     sink.text(artifactDescription);
1003                 } else {
1004                     sink.text(getI18nString("index", "nodescription"));
1005                 }
1006                 sink.paragraph_();
1007 
1008                 if (artifactUrl != null && !artifactUrl.isEmpty()) {
1009                     sink.paragraph();
1010                     sink.bold();
1011                     sink.text(getI18nString("column.url") + ": ");
1012                     sink.bold_();
1013                     if (ProjectInfoReportUtils.isArtifactUrlValid(artifactUrl)) {
1014                         sink.link(artifactUrl);
1015                         sink.text(artifactUrl);
1016                         sink.link_();
1017                     } else {
1018                         sink.text(artifactUrl);
1019                     }
1020                     sink.paragraph_();
1021                 }
1022 
1023                 sink.paragraph();
1024                 sink.bold();
1025                 sink.text(getI18nString("licenses", "title") + ": ");
1026                 sink.bold_();
1027                 if (!licenses.isEmpty()) {
1028 
1029                     for (Iterator<License> it = licenses.iterator(); it.hasNext(); ) {
1030                         License license = it.next();
1031 
1032                         String licenseName = license.getName();
1033                         if (licenseMappings != null && licenseMappings.containsKey(licenseName)) {
1034                             licenseName = licenseMappings.get(licenseName);
1035                         }
1036                         if (licenseName == null || licenseName.isEmpty()) {
1037                             licenseName = getI18nString("unnamed");
1038                         }
1039 
1040                         String licenseUrl = license.getUrl();
1041 
1042                         if (licenseUrl != null) {
1043                             sink.link(licenseUrl);
1044                         }
1045                         sink.text(licenseName);
1046 
1047                         if (licenseUrl != null) {
1048                             sink.link_();
1049                         }
1050 
1051                         if (it.hasNext()) {
1052                             sink.text(", ");
1053                         }
1054 
1055                         licenseMap.put(licenseName, artifactName);
1056                     }
1057                 } else {
1058                     sink.text(getI18nString("licenses", "nolicense"));
1059 
1060                     licenseMap.put(unknownLicenseMessage, artifactName);
1061                 }
1062                 sink.paragraph_();
1063 
1064                 sink.tableCell_();
1065                 sink.tableRow_();
1066 
1067                 endTable();
1068             } catch (ProjectBuildingException e) {
1069                 sink.text(getI18nString("index", "nodescription"));
1070                 if (log.isDebugEnabled()) {
1071                     log.debug(
1072                             "Unable to create Maven project from repository for artifact '" + artifact.getId() + "'",
1073                             e);
1074                 } else {
1075                     log.info("Unable to create Maven project from repository for artifact '" + artifact.getId()
1076                             + "', for more information run with -X");
1077                 }
1078             }
1079         } else {
1080             startTable();
1081 
1082             sink.tableRow();
1083             sink.tableHeaderCell();
1084             sink.text(id);
1085             sink.tableHeaderCell_();
1086             sink.tableRow_();
1087 
1088             sink.tableRow();
1089             sink.tableCell();
1090 
1091             sink.paragraph();
1092             sink.bold();
1093             sink.text(getI18nString("column.description") + ": ");
1094             sink.bold_();
1095             sink.text(getI18nString("index", "nodescription"));
1096             sink.paragraph_();
1097 
1098             if (artifact.getFile() != null) {
1099                 sink.paragraph();
1100                 sink.bold();
1101                 sink.text(getI18nString("column.url") + ": ");
1102                 sink.bold_();
1103                 sink.text(artifact.getFile().getAbsolutePath());
1104                 sink.paragraph_();
1105             }
1106 
1107             sink.tableCell_();
1108             sink.tableRow_();
1109 
1110             endTable();
1111         }
1112 
1113         sink.rawText("</div>");
1114     }
1115 
1116     private void printGroupedLicenses() {
1117         for (Map.Entry<String, Object> entry : licenseMap.entrySet()) {
1118             String licenseName = entry.getKey();
1119             if (licenseName == null || licenseName.isEmpty()) {
1120                 licenseName = getI18nString("unnamed");
1121             }
1122 
1123             sink.paragraph();
1124             sink.bold();
1125             sink.text(licenseName);
1126             sink.text(": ");
1127             sink.bold_();
1128 
1129             @SuppressWarnings("unchecked")
1130             SortedSet<String> projects = (SortedSet<String>) entry.getValue();
1131 
1132             for (Iterator<String> iterator = projects.iterator(); iterator.hasNext(); ) {
1133                 String projectName = iterator.next();
1134                 sink.text(projectName);
1135                 if (iterator.hasNext()) {
1136                     sink.text(", ");
1137                 }
1138             }
1139 
1140             sink.paragraph_();
1141         }
1142     }
1143 
1144     /**
1145      * Resolves all given artifacts with {@link RepositoryUtils}.
1146      *
1147      ** @param artifacts not null
1148      */
1149     private void resolveAtrifacts(List<Artifact> artifacts) {
1150         for (Artifact artifact : artifacts) {
1151             // TODO site:run Why do we need to resolve this...
1152             if (artifact.getFile() == null) {
1153                 if (Artifact.SCOPE_SYSTEM.equals(artifact.getScope())) {
1154                     // can not resolve system scope artifact file
1155                     continue;
1156                 }
1157 
1158                 try {
1159                     repoUtils.resolve(artifact);
1160                 } catch (ArtifactResolverException e) {
1161                     log.error("Artifact " + artifact.getId() + " can't be resolved.", e);
1162                     continue;
1163                 }
1164 
1165                 if (artifact.getFile() == null) {
1166                     log.error("Artifact " + artifact.getId() + " has no file, even after resolution.");
1167                 }
1168             }
1169         }
1170     }
1171 
1172     /**
1173      * @param artifacts not null
1174      * @return <code>true</code> if one artifact in the list has a classifier, <code>false</code> otherwise.
1175      */
1176     private boolean hasClassifier(List<Artifact> artifacts) {
1177         for (Artifact artifact : artifacts) {
1178             if (StringUtils.isNotEmpty(artifact.getClassifier())) {
1179                 return true;
1180             }
1181         }
1182 
1183         return false;
1184     }
1185 
1186     /**
1187      * @param artifacts not null
1188      * @return <code>true</code> if one artifact in the list is optional, <code>false</code> otherwise.
1189      */
1190     private boolean hasOptional(List<Artifact> artifacts) {
1191         for (Artifact artifact : artifacts) {
1192             if (artifact.isOptional()) {
1193                 return true;
1194             }
1195         }
1196 
1197         return false;
1198     }
1199 
1200     /**
1201      * @param artifacts not null
1202      * @return <code>true</code> if one artifact in the list is sealed, <code>false</code> otherwise.
1203      */
1204     private boolean hasSealed(List<Artifact> artifacts) {
1205         for (Artifact artifact : artifacts) {
1206             if (artifact.getFile() != null
1207                     && JAR_SUBTYPE.contains(artifact.getType().toLowerCase())) {
1208                 try {
1209                     JarData jarDetails = dependencies.getJarDependencyDetails(artifact);
1210                     if (jarDetails.isSealed()) {
1211                         return true;
1212                     }
1213                 } catch (IOException e) {
1214                     log.error("Artifact " + artifact.getId() + " caused IOException: " + e.getMessage(), e);
1215                 }
1216             }
1217         }
1218         return false;
1219     }
1220 
1221     // CHECKSTYLE_OFF: LineLength
1222     /**
1223      * Formats file length with the associated <a href="https://en.wikipedia.org/wiki/Metric_prefix">SI</a> prefix
1224      * (GB, MB, kB) and using the pattern <code>###0.#</code> by default.
1225      *
1226      * @see <a href="https://en.wikipedia.org/wiki/Metric_prefix">https://en.wikipedia.org/wiki/Metric_prefix</a>
1227      * @see <a href="https://en.wikipedia.org/wiki/Binary_prefix">https://en.wikipedia.org/wiki/Binary_prefix</a>
1228      * @see <a
1229      *      href="https://en.wikipedia.org/wiki/Octet_%28computing%29">https://en.wikipedia.org/wiki/Octet_(computing)</a>
1230      */
1231     // CHECKSTYLE_ON: LineLength
1232     static class FileDecimalFormat extends DecimalFormat {
1233         private static final long serialVersionUID = 4062503546523610081L;
1234 
1235         private final I18N i18n;
1236 
1237         private final Locale locale;
1238 
1239         /**
1240          * Default constructor
1241          *
1242          * @param i18n
1243          * @param locale
1244          */
1245         FileDecimalFormat(I18N i18n, Locale locale) {
1246             super("###0.#");
1247 
1248             this.i18n = i18n;
1249             this.locale = locale;
1250         }
1251 
1252         /** {@inheritDoc} */
1253         @Override
1254         public StringBuffer format(long fs, StringBuffer result, FieldPosition fieldPosition) {
1255             if (fs > 1000 * 1000 * 1000) {
1256                 result = super.format((float) fs / (1000 * 1000 * 1000), result, fieldPosition);
1257                 result.append(" ").append(getString("report.dependencies.file.details.column.size.gb"));
1258                 return result;
1259             }
1260 
1261             if (fs > 1000 * 1000) {
1262                 result = super.format((float) fs / (1000 * 1000), result, fieldPosition);
1263                 result.append(" ").append(getString("report.dependencies.file.details.column.size.mb"));
1264                 return result;
1265             }
1266 
1267             result = super.format((float) fs / 1000, result, fieldPosition);
1268             result.append(" ").append(getString("report.dependencies.file.details.column.size.kb"));
1269             return result;
1270         }
1271 
1272         private String getString(String key) {
1273             return i18n.getString("project-info-reports", locale, key);
1274         }
1275     }
1276 
1277     /**
1278      * Combine total and total by scope in a cell.
1279      */
1280     static class TotalCell {
1281         public enum SummaryTableRowOrder {
1282             // Do not change the physical order of these values
1283             TOTALS {
1284                 @Override
1285                 public void addTotal(TotalCell cell, long value) {
1286                     cell.total += value;
1287                 }
1288 
1289                 @Override
1290                 public long getTotal(TotalCell cell) {
1291                     return cell.total;
1292                 }
1293 
1294                 @Override
1295                 protected String formatMaxJavaVersionForScope(
1296                         MessageFormat javaVersionFormat,
1297                         double highestTestJavaVersion,
1298                         double highestNonTestJavaVersion) {
1299                     double highestJavaVersion = Math.max(highestTestJavaVersion, highestNonTestJavaVersion);
1300                     return javaVersionFormat.format(new Object[] {highestJavaVersion});
1301                 }
1302             },
1303             COMPILE_SCOPE(Artifact.SCOPE_COMPILE) {
1304                 @Override
1305                 public void addTotal(TotalCell cell, long value) {
1306                     cell.totalCompileScope += value;
1307                 }
1308 
1309                 @Override
1310                 public long getTotal(TotalCell cell) {
1311                     return cell.totalCompileScope;
1312                 }
1313             },
1314             RUNTIME_SCOPE(Artifact.SCOPE_RUNTIME) {
1315                 @Override
1316                 public void addTotal(TotalCell cell, long value) {
1317                     cell.totalRuntimeScope += value;
1318                 }
1319 
1320                 @Override
1321                 public long getTotal(TotalCell cell) {
1322                     return cell.totalRuntimeScope;
1323                 }
1324             },
1325             PROVIDED_SCOPE(Artifact.SCOPE_PROVIDED) {
1326                 @Override
1327                 public void addTotal(TotalCell cell, long value) {
1328                     cell.totalProvidedScope += value;
1329                 }
1330 
1331                 @Override
1332                 public long getTotal(TotalCell cell) {
1333                     return cell.totalProvidedScope;
1334                 }
1335             },
1336             SYSTEM_SCOPE(Artifact.SCOPE_SYSTEM) {
1337                 @Override
1338                 public void addTotal(TotalCell cell, long value) {
1339                     cell.totalSystemScope += value;
1340                 }
1341 
1342                 @Override
1343                 public long getTotal(TotalCell cell) {
1344                     return cell.totalSystemScope;
1345                 }
1346             },
1347             TEST_SCOPE(Artifact.SCOPE_TEST) {
1348                 @Override
1349                 public void addTotal(TotalCell cell, long value) {
1350                     cell.totalTestScope += value;
1351                 }
1352 
1353                 @Override
1354                 public long getTotal(TotalCell cell) {
1355                     return cell.totalTestScope;
1356                 }
1357 
1358                 @Override
1359                 protected String formatMaxJavaVersionForScope(
1360                         MessageFormat javaVersionFormat,
1361                         double highestTestJavaVersion,
1362                         double highestNonTestJavaVersion) {
1363                     return javaVersionFormat.format(new Object[] {highestTestJavaVersion});
1364                 }
1365             };
1366 
1367             private static final Map<String, SummaryTableRowOrder> MAP_BY_SCOPE = new HashMap<>();
1368 
1369             static {
1370                 // scope string => enum mapping
1371                 for (SummaryTableRowOrder e : SummaryTableRowOrder.values()) {
1372                     MAP_BY_SCOPE.put(e.getScope(), e);
1373                 }
1374             }
1375 
1376             public static SummaryTableRowOrder fromScope(String scope) {
1377                 return MAP_BY_SCOPE.get(scope);
1378             }
1379 
1380             private String scope;
1381 
1382             SummaryTableRowOrder() {
1383                 this(null);
1384             }
1385 
1386             SummaryTableRowOrder(String scope) {
1387                 this.scope = scope;
1388             }
1389 
1390             public String getScope() {
1391                 return this.scope;
1392             }
1393 
1394             protected String formatMaxJavaVersionForScope(
1395                     MessageFormat javaVersionFormat, double highestTestJavaVersion, double highestNonTestJavaVersion) {
1396                 return javaVersionFormat.format(new Object[] {highestNonTestJavaVersion});
1397             }
1398 
1399             public abstract void addTotal(TotalCell cell, long value);
1400 
1401             public abstract long getTotal(TotalCell cell);
1402         }
1403 
1404         DecimalFormat decimalFormat;
1405 
1406         long total = 0;
1407 
1408         long totalCompileScope = 0;
1409 
1410         long totalTestScope = 0;
1411 
1412         long totalRuntimeScope = 0;
1413 
1414         long totalProvidedScope = 0;
1415 
1416         long totalSystemScope = 0;
1417 
1418         TotalCell() {}
1419 
1420         TotalCell(DecimalFormat decimalFormat) {
1421             this.decimalFormat = decimalFormat;
1422         }
1423 
1424         void incrementTotal(String scope) {
1425             addTotal(1, scope);
1426         }
1427 
1428         String getTotalString(SummaryTableRowOrder currentRow) {
1429             long totalString = currentRow.getTotal(this);
1430 
1431             if (totalString <= 0) {
1432                 return "";
1433             }
1434 
1435             StringBuilder sb = new StringBuilder();
1436             if (currentRow.compareTo(SummaryTableRowOrder.COMPILE_SCOPE) >= 0) {
1437                 sb.append(currentRow.getScope()).append(": ");
1438             }
1439             if (decimalFormat != null) {
1440                 sb.append(decimalFormat.format(currentRow.getTotal(this)));
1441             } else {
1442                 sb.append(currentRow.getTotal(this));
1443             }
1444 
1445             return sb.toString();
1446         }
1447 
1448         void addTotal(long add, String scope) {
1449             SummaryTableRowOrder.TOTALS.addTotal(this, add);
1450             SummaryTableRowOrder currentRow = SummaryTableRowOrder.fromScope(scope);
1451             currentRow.addTotal(this, add);
1452         }
1453 
1454         /** {@inheritDoc} */
1455         public String toString() {
1456             StringBuilder sb = new StringBuilder();
1457             if (decimalFormat != null) {
1458                 sb.append(decimalFormat.format(total));
1459             } else {
1460                 sb.append(total);
1461             }
1462 
1463             sb.append(" (");
1464 
1465             boolean needSeparator = false;
1466             for (int i = SummaryTableRowOrder.COMPILE_SCOPE.ordinal();
1467                     i < SummaryTableRowOrder.TEST_SCOPE.ordinal();
1468                     i++) {
1469                 SummaryTableRowOrder currentRow = SummaryTableRowOrder.values()[i];
1470                 if (currentRow.getTotal(this) > 0) {
1471                     if (needSeparator) {
1472                         sb.append(", ");
1473                     }
1474                     sb.append(getTotalString(currentRow));
1475                     needSeparator = true;
1476                 }
1477             }
1478 
1479             sb.append(")");
1480 
1481             return sb.toString();
1482         }
1483     }
1484 }