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