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