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 java.io.File;
22  import java.io.IOException;
23  import java.io.PrintWriter;
24  import java.io.StringWriter;
25  import java.text.DecimalFormat;
26  import java.text.DecimalFormatSymbols;
27  import java.text.FieldPosition;
28  import java.util.ArrayList;
29  import java.util.Collections;
30  import java.util.Comparator;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.SortedSet;
39  import java.util.TreeSet;
40  
41  import org.apache.maven.artifact.Artifact;
42  import org.apache.maven.doxia.sink.Sink;
43  import org.apache.maven.doxia.sink.SinkEventAttributes;
44  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
45  import org.apache.maven.doxia.util.HtmlTools;
46  import org.apache.maven.model.License;
47  import org.apache.maven.plugin.logging.Log;
48  import org.apache.maven.project.MavenProject;
49  import org.apache.maven.project.ProjectBuilder;
50  import org.apache.maven.project.ProjectBuildingException;
51  import org.apache.maven.project.ProjectBuildingRequest;
52  import org.apache.maven.report.projectinfo.AbstractProjectInfoRenderer;
53  import org.apache.maven.report.projectinfo.LicenseMapping;
54  import org.apache.maven.report.projectinfo.ProjectInfoReportUtils;
55  import org.apache.maven.report.projectinfo.dependencies.Dependencies;
56  import org.apache.maven.report.projectinfo.dependencies.DependenciesReportConfiguration;
57  import org.apache.maven.report.projectinfo.dependencies.RepositoryUtils;
58  import org.apache.maven.repository.RepositorySystem;
59  import org.apache.maven.shared.dependency.graph.DependencyNode;
60  import org.apache.maven.shared.jar.JarData;
61  import org.apache.maven.shared.transfer.artifact.resolve.ArtifactResolverException;
62  import org.codehaus.plexus.i18n.I18N;
63  import org.codehaus.plexus.util.StringUtils;
64  
65  /**
66   * Renderer the dependencies report.
67   *
68   * @version $Id$
69   * @since 2.1
70   */
71  public class DependenciesRenderer extends AbstractProjectInfoRenderer {
72      /** URL for the 'icon_info_sml.gif' image */
73      private static final String IMG_INFO_URL = "./images/icon_info_sml.gif";
74  
75      /** URL for the 'close.gif' image */
76      private static final String IMG_CLOSE_URL = "./images/close.gif";
77  
78      /** Used to format decimal values in the "Dependency File Details" table */
79      protected static final DecimalFormat DEFAULT_DECIMAL_FORMAT = new DecimalFormat("###0");
80  
81      private static final Set<String> JAR_SUBTYPE;
82  
83      private final DependencyNode dependencyNode;
84  
85      private final Dependencies dependencies;
86  
87      private final DependenciesReportConfiguration configuration;
88  
89      private final Log log;
90  
91      private final RepositoryUtils repoUtils;
92  
93      /** Used to format file length values */
94      private final DecimalFormat fileLengthDecimalFormat;
95  
96      /**
97       * @since 2.1.1
98       */
99      private int section;
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 RepositorySystem repositorySystem;
125 
126     private final ProjectBuilder projectBuilder;
127 
128     private final ProjectBuildingRequest buildingRequest;
129 
130     private final Map<String, String> licenseMappings;
131 
132     static {
133         Set<String> jarSubtype = new HashSet<>();
134         jarSubtype.add("jar");
135         jarSubtype.add("war");
136         jarSubtype.add("ear");
137         jarSubtype.add("sar");
138         jarSubtype.add("rar");
139         jarSubtype.add("par");
140         jarSubtype.add("ejb");
141         JAR_SUBTYPE = Collections.unmodifiableSet(jarSubtype);
142     }
143 
144     /**
145      * Default constructor.
146      *
147      * @param sink {@link Sink}
148      * @param locale {@link Locale}
149      * @param i18n {@link I18N}
150      * @param log {@link Log}
151      * @param dependencies {@link Dependencies}
152      * @param dependencyTreeNode {@link DependencyNode}
153      * @param config {@link DependenciesReportConfiguration}
154      * @param repoUtils {@link RepositoryUtils}
155      * @param repositorySystem {@link RepositorySystem}
156      * @param projectBuilder {@link ProjectBuilder}
157      * @param buildingRequest {@link ProjectBuildingRequest}
158      * @param licenseMappings {@link LicenseMapping}
159      */
160     public DependenciesRenderer(
161             Sink sink,
162             Locale locale,
163             I18N i18n,
164             Log log,
165             Dependencies dependencies,
166             DependencyNode dependencyTreeNode,
167             DependenciesReportConfiguration config,
168             RepositoryUtils repoUtils,
169             RepositorySystem repositorySystem,
170             ProjectBuilder projectBuilder,
171             ProjectBuildingRequest buildingRequest,
172             Map<String, String> licenseMappings) {
173         super(sink, i18n, locale);
174 
175         this.log = log;
176         this.dependencies = dependencies;
177         this.dependencyNode = dependencyTreeNode;
178         this.repoUtils = repoUtils;
179         this.configuration = config;
180         this.repositorySystem = repositorySystem;
181         this.projectBuilder = projectBuilder;
182         this.buildingRequest = buildingRequest;
183         this.licenseMappings = licenseMappings;
184 
185         // Using the right set of symbols depending of the locale
186         DEFAULT_DECIMAL_FORMAT.setDecimalFormatSymbols(new DecimalFormatSymbols(locale));
187 
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     public 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 anchor, String name) {
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("");
439         pw.println("<script language=\"javascript\" type=\"text/javascript\">");
440         pw.println("      function toggleDependencyDetails( divId, imgId )");
441         pw.println("      {");
442         pw.println("        var div = document.getElementById( divId );");
443         pw.println("        var img = document.getElementById( imgId );");
444         pw.println("        if( div.style.display == '' )");
445         pw.println("        {");
446         pw.println("          div.style.display = 'none';");
447         pw.printf("          img.src='%s';%n", IMG_INFO_URL);
448         pw.printf("          img.alt='%s';%n", getI18nString("graph.icon.information"));
449         pw.println("        }");
450         pw.println("        else");
451         pw.println("        {");
452         pw.println("          div.style.display = '';");
453         pw.printf("          img.src='%s';%n", IMG_CLOSE_URL);
454         pw.printf("          img.alt='%s';%n", getI18nString("graph.icon.close"));
455         pw.println("        }");
456         pw.println("      }");
457         pw.println("</script>");
458 
459         sink.rawText(sw.toString());
460 
461         // for Dependencies Graph Tree
462         startSection(getI18nString("graph.tree.title"));
463 
464         sink.list();
465         printDependencyListing(dependencyNode);
466         sink.list_();
467 
468         endSection();
469     }
470 
471     private void renderSectionDependencyFileDetails() {
472         startSection(getI18nString("file.details.title"));
473 
474         List<Artifact> alldeps = dependencies.getAllDependencies();
475         Collections.sort(alldeps, getArtifactComparator());
476 
477         resolveAtrifacts(alldeps);
478 
479         // i18n
480         String filename = getI18nString("file.details.column.file");
481         String size = getI18nString("file.details.column.size");
482         String entries = getI18nString("file.details.column.entries");
483         String classes = getI18nString("file.details.column.classes");
484         String packages = getI18nString("file.details.column.packages");
485         String javaVersion = getI18nString("file.details.column.javaVersion");
486         String debugInformation = getI18nString("file.details.column.debuginformation");
487         String debugInformationTitle = getI18nString("file.details.columntitle.debuginformation");
488         String debugInformationCellYes = getI18nString("file.details.cell.debuginformation.yes");
489         String debugInformationCellNo = getI18nString("file.details.cell.debuginformation.no");
490         String aSealed = getI18nString("file.details.column.sealed");
491         String sealedCellYes = getI18nString("file.details.cell.sealed.yes");
492         String sealedCellNo = getI18nString("file.details.cell.sealed.no");
493 
494         int[] justification = new int[] {
495             Sink.JUSTIFY_LEFT,
496             Sink.JUSTIFY_RIGHT,
497             Sink.JUSTIFY_RIGHT,
498             Sink.JUSTIFY_RIGHT,
499             Sink.JUSTIFY_RIGHT,
500             Sink.JUSTIFY_CENTER,
501             Sink.JUSTIFY_CENTER,
502             Sink.JUSTIFY_CENTER
503         };
504 
505         startTable(justification, false);
506 
507         TotalCell totaldeps = new TotalCell(DEFAULT_DECIMAL_FORMAT);
508         TotalCell totaldepsize = new TotalCell(fileLengthDecimalFormat);
509         TotalCell totalentries = new TotalCell(DEFAULT_DECIMAL_FORMAT);
510         TotalCell totalclasses = new TotalCell(DEFAULT_DECIMAL_FORMAT);
511         TotalCell totalpackages = new TotalCell(DEFAULT_DECIMAL_FORMAT);
512         double highestJavaVersion = 0.0;
513         TotalCell totalDebugInformation = new TotalCell(DEFAULT_DECIMAL_FORMAT);
514         TotalCell totalsealed = new TotalCell(DEFAULT_DECIMAL_FORMAT);
515 
516         boolean hasSealed = hasSealed(alldeps);
517 
518         // Table header
519         String[] tableHeader;
520         String[] tableHeaderTitles;
521         if (hasSealed) {
522             tableHeader =
523                     new String[] {filename, size, entries, classes, packages, javaVersion, debugInformation, aSealed};
524             tableHeaderTitles = new String[] {null, null, null, null, null, null, debugInformationTitle, null};
525         } else {
526             tableHeader = new String[] {filename, size, entries, classes, packages, javaVersion, debugInformation};
527             tableHeaderTitles = new String[] {null, null, null, null, null, null, debugInformationTitle};
528         }
529         tableHeader(tableHeader, tableHeaderTitles);
530 
531         // Table rows
532         for (Artifact artifact : alldeps) {
533             if (artifact.getFile() == null) {
534                 log.warn("Artifact " + artifact.getId() + " has no file"
535                         + " and won't be listed in dependency files details.");
536                 continue;
537             }
538 
539             File artifactFile = dependencies.getFile(artifact);
540 
541             totaldeps.incrementTotal(artifact.getScope());
542             totaldepsize.addTotal(artifactFile.length(), artifact.getScope());
543 
544             if (JAR_SUBTYPE.contains(artifact.getType().toLowerCase())) {
545                 try {
546                     JarData jarDetails = dependencies.getJarDependencyDetails(artifact);
547 
548                     String debugInformationCellValue = debugInformationCellNo;
549                     if (jarDetails.isDebugPresent()) {
550                         debugInformationCellValue = debugInformationCellYes;
551                         totalDebugInformation.incrementTotal(artifact.getScope());
552                     }
553 
554                     totalentries.addTotal(jarDetails.getNumEntries(), artifact.getScope());
555                     totalclasses.addTotal(jarDetails.getNumClasses(), artifact.getScope());
556                     totalpackages.addTotal(jarDetails.getNumPackages(), artifact.getScope());
557 
558                     try {
559                         if (jarDetails.getJdkRevision() != null) {
560                             highestJavaVersion =
561                                     Math.max(highestJavaVersion, Double.parseDouble(jarDetails.getJdkRevision()));
562                         }
563                     } catch (NumberFormatException e) {
564                         // ignore
565                     }
566 
567                     String sealedCellValue = sealedCellNo;
568                     if (jarDetails.isSealed()) {
569                         sealedCellValue = sealedCellYes;
570                         totalsealed.incrementTotal(artifact.getScope());
571                     }
572 
573                     String name = artifactFile.getName();
574                     String fileLength = fileLengthDecimalFormat.format(artifactFile.length());
575 
576                     if (artifactFile.isDirectory()) {
577                         File parent = artifactFile.getParentFile();
578                         name = parent.getParentFile().getName() + '/' + parent.getName() + '/' + artifactFile.getName();
579                         fileLength = "-";
580                     }
581 
582                     tableRow(hasSealed, new String[] {
583                         name,
584                         fileLength,
585                         DEFAULT_DECIMAL_FORMAT.format(jarDetails.getNumEntries()),
586                         DEFAULT_DECIMAL_FORMAT.format(jarDetails.getNumClasses()),
587                         DEFAULT_DECIMAL_FORMAT.format(jarDetails.getNumPackages()),
588                         jarDetails.getJdkRevision(),
589                         debugInformationCellValue,
590                         sealedCellValue
591                     });
592                 } catch (IOException e) {
593                     createExceptionInfoTableRow(artifact, artifactFile, e, hasSealed);
594                 }
595             } else {
596                 tableRow(hasSealed, new String[] {
597                     artifactFile.getName(),
598                     fileLengthDecimalFormat.format(artifactFile.length()),
599                     "",
600                     "",
601                     "",
602                     "",
603                     "",
604                     ""
605                 });
606             }
607         }
608 
609         // Total raws
610         tableHeader[0] = getI18nString("file.details.total");
611         tableHeader(tableHeader);
612 
613         justification[0] = Sink.JUSTIFY_RIGHT;
614         justification[6] = Sink.JUSTIFY_RIGHT;
615 
616         for (int i = -1; i < TotalCell.SCOPES_COUNT; i++) {
617             if (totaldeps.getTotal(i) > 0) {
618                 tableRow(hasSealed, new String[] {
619                     totaldeps.getTotalString(i),
620                     totaldepsize.getTotalString(i),
621                     totalentries.getTotalString(i),
622                     totalclasses.getTotalString(i),
623                     totalpackages.getTotalString(i),
624                     (i < 0) ? String.valueOf(highestJavaVersion) : "",
625                     totalDebugInformation.getTotalString(i),
626                     totalsealed.getTotalString(i)
627                 });
628             }
629         }
630 
631         endTable();
632         endSection();
633     }
634 
635     // Almost as same as in the abstract class but includes the title attribute
636     private void tableHeader(String[] content, String[] titles) {
637         sink.tableRow();
638 
639         if (content != null) {
640             if (titles != null && content.length != titles.length) {
641                 throw new IllegalArgumentException("Length of title array must equal the length of the content array");
642             }
643 
644             for (int i = 0; i < content.length; i++) {
645                 if (titles != null) {
646                     tableHeaderCell(content[i], titles[i]);
647                 } else {
648                     tableHeaderCell(content[i]);
649                 }
650             }
651         }
652 
653         sink.tableRow_();
654     }
655 
656     private void tableHeaderCell(String text, String title) {
657         if (title != null) {
658             sink.tableHeaderCell(new SinkEventAttributeSet(SinkEventAttributes.TITLE, title));
659         } else {
660             sink.tableHeaderCell();
661         }
662 
663         text(text);
664 
665         sink.tableHeaderCell_();
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(anchorByScope, scope);
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         String url = ProjectInfoReportUtils.getArtifactUrl(repositorySystem, artifact, projectBuilder, buildingRequest);
745         String artifactIdCell = ProjectInfoReportUtils.getArtifactIdCell(artifact.getArtifactId(), url);
746 
747         MavenProject artifactProject;
748         StringBuilder sb = new StringBuilder();
749         try {
750             artifactProject = repoUtils.getMavenProjectFromRepository(artifact);
751 
752             List<License> licenses = artifactProject.getLicenses();
753             for (License license : licenses) {
754                 String name = license.getName();
755                 if (licenseMappings != null && licenseMappings.containsKey(name)) {
756                     name = licenseMappings.get(name);
757                 }
758                 sb.append(ProjectInfoReportUtils.getArtifactIdCell(name, license.getUrl()));
759             }
760         } catch (ProjectBuildingException e) {
761             if (log.isDebugEnabled()) {
762                 log.debug("Unable to create Maven project from repository for artifact '" + artifact.getId() + "'", e);
763             } else {
764                 log.info("Unable to create Maven project from repository for artifact '" + artifact.getId()
765                         + "', for more information run with -X");
766             }
767         }
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<DependencyNode>();
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                 sink.table();
853                 sink.tableRows(null, false);
854 
855                 sink.tableRow();
856                 sink.tableHeaderCell();
857                 sink.text(artifactName);
858                 sink.tableHeaderCell_();
859                 sink.tableRow_();
860 
861                 sink.tableRow();
862                 sink.tableCell();
863 
864                 sink.paragraph();
865                 sink.bold();
866                 sink.text(getI18nString("column.description") + ": ");
867                 sink.bold_();
868                 if (StringUtils.isNotEmpty(artifactDescription)) {
869                     sink.text(artifactDescription);
870                 } else {
871                     sink.text(getI18nString("index", "nodescription"));
872                 }
873                 sink.paragraph_();
874 
875                 if (StringUtils.isNotEmpty(artifactUrl)) {
876                     sink.paragraph();
877                     sink.bold();
878                     sink.text(getI18nString("column.url") + ": ");
879                     sink.bold_();
880                     if (ProjectInfoReportUtils.isArtifactUrlValid(artifactUrl)) {
881                         sink.link(artifactUrl);
882                         sink.text(artifactUrl);
883                         sink.link_();
884                     } else {
885                         sink.text(artifactUrl);
886                     }
887                     sink.paragraph_();
888                 }
889 
890                 sink.paragraph();
891                 sink.bold();
892                 sink.text(getI18nString("licenses", "title") + ": ");
893                 sink.bold_();
894                 if (!licenses.isEmpty()) {
895 
896                     for (Iterator<License> it = licenses.iterator(); it.hasNext(); ) {
897                         License license = it.next();
898 
899                         String licenseName = license.getName();
900                         if (licenseMappings != null && licenseMappings.containsKey(licenseName)) {
901                             licenseName = licenseMappings.get(licenseName);
902                         }
903                         if (StringUtils.isEmpty(licenseName)) {
904                             licenseName = getI18nString("unnamed");
905                         }
906 
907                         String licenseUrl = license.getUrl();
908 
909                         if (licenseUrl != null) {
910                             sink.link(licenseUrl);
911                         }
912                         sink.text(licenseName);
913 
914                         if (licenseUrl != null) {
915                             sink.link_();
916                         }
917 
918                         if (it.hasNext()) {
919                             sink.text(", ");
920                         }
921 
922                         licenseMap.put(licenseName, artifactName);
923                     }
924                 } else {
925                     sink.text(getI18nString("licenses", "nolicense"));
926 
927                     licenseMap.put(unknownLicenseMessage, artifactName);
928                 }
929                 sink.paragraph_();
930 
931                 sink.tableCell_();
932                 sink.tableRow_();
933 
934                 sink.tableRows_();
935                 sink.table_();
936             } catch (ProjectBuildingException e) {
937                 sink.text(getI18nString("index", "nodescription"));
938                 if (log.isDebugEnabled()) {
939                     log.debug(
940                             "Unable to create Maven project from repository for artifact '" + artifact.getId() + "'",
941                             e);
942                 } else {
943                     log.info("Unable to create Maven project from repository for artifact '" + artifact.getId()
944                             + "', for more information run with -X");
945                 }
946             }
947         } else {
948             sink.table();
949             sink.tableRows(null, false);
950 
951             sink.tableRow();
952             sink.tableHeaderCell();
953             sink.text(id);
954             sink.tableHeaderCell_();
955             sink.tableRow_();
956 
957             sink.tableRow();
958             sink.tableCell();
959 
960             sink.paragraph();
961             sink.bold();
962             sink.text(getI18nString("column.description") + ": ");
963             sink.bold_();
964             sink.text(getI18nString("index", "nodescription"));
965             sink.paragraph_();
966 
967             if (artifact.getFile() != null) {
968                 sink.paragraph();
969                 sink.bold();
970                 sink.text(getI18nString("column.url") + ": ");
971                 sink.bold_();
972                 sink.text(artifact.getFile().getAbsolutePath());
973                 sink.paragraph_();
974             }
975 
976             sink.tableCell_();
977             sink.tableRow_();
978 
979             sink.tableRows_();
980             sink.table_();
981         }
982 
983         sink.rawText("</div>");
984     }
985 
986     private void printGroupedLicenses() {
987         for (Map.Entry<String, Object> entry : licenseMap.entrySet()) {
988             String licenseName = entry.getKey();
989             if (StringUtils.isEmpty(licenseName)) {
990                 licenseName = getI18nString("unnamed");
991             }
992 
993             sink.paragraph();
994             sink.bold();
995             sink.text(licenseName);
996             sink.text(": ");
997             sink.bold_();
998 
999             @SuppressWarnings("unchecked")
1000             SortedSet<String> projects = (SortedSet<String>) entry.getValue();
1001 
1002             for (Iterator<String> iterator = projects.iterator(); iterator.hasNext(); ) {
1003                 String projectName = iterator.next();
1004                 sink.text(projectName);
1005                 if (iterator.hasNext()) {
1006                     sink.text(", ");
1007                 }
1008             }
1009 
1010             sink.paragraph_();
1011         }
1012     }
1013 
1014     /**
1015      * Resolves all given artifacts with {@link RepositoryUtils}.
1016      *
1017      ** @param artifacts not null
1018      */
1019     private void resolveAtrifacts(List<Artifact> artifacts) {
1020         for (Artifact artifact : artifacts) {
1021             // TODO site:run Why do we need to resolve this...
1022             if (artifact.getFile() == null) {
1023                 if (Artifact.SCOPE_SYSTEM.equals(artifact.getScope())) {
1024                     // can not resolve system scope artifact file
1025                     continue;
1026                 }
1027 
1028                 try {
1029                     repoUtils.resolve(artifact);
1030                 } catch (ArtifactResolverException e) {
1031                     log.error("Artifact " + artifact.getId() + " can't be resolved.", e);
1032                     continue;
1033                 }
1034 
1035                 if (artifact.getFile() == null) {
1036                     log.error("Artifact " + artifact.getId() + " has no file, even after resolution.");
1037                 }
1038             }
1039         }
1040     }
1041 
1042     /**
1043      * @param artifacts not null
1044      * @return <code>true</code> if one artifact in the list has a classifier, <code>false</code> otherwise.
1045      */
1046     private boolean hasClassifier(List<Artifact> artifacts) {
1047         for (Artifact artifact : artifacts) {
1048             if (StringUtils.isNotEmpty(artifact.getClassifier())) {
1049                 return true;
1050             }
1051         }
1052 
1053         return false;
1054     }
1055 
1056     /**
1057      * @param artifacts not null
1058      * @return <code>true</code> if one artifact in the list is optional, <code>false</code> otherwise.
1059      */
1060     private boolean hasOptional(List<Artifact> artifacts) {
1061         for (Artifact artifact : artifacts) {
1062             if (artifact.isOptional()) {
1063                 return true;
1064             }
1065         }
1066 
1067         return false;
1068     }
1069 
1070     /**
1071      * @param artifacts not null
1072      * @return <code>true</code> if one artifact in the list is sealed, <code>false</code> otherwise.
1073      */
1074     private boolean hasSealed(List<Artifact> artifacts) {
1075         for (Artifact artifact : artifacts) {
1076             if (artifact.getFile() != null
1077                     && JAR_SUBTYPE.contains(artifact.getType().toLowerCase())) {
1078                 try {
1079                     JarData jarDetails = dependencies.getJarDependencyDetails(artifact);
1080                     if (jarDetails.isSealed()) {
1081                         return true;
1082                     }
1083                 } catch (IOException e) {
1084                     log.error("Artifact " + artifact.getId() + " caused IOException: " + e.getMessage(), e);
1085                 }
1086             }
1087         }
1088         return false;
1089     }
1090 
1091     // CHECKSTYLE_OFF: LineLength
1092     /**
1093      * Formats file length with the associated <a href="https://en.wikipedia.org/wiki/Metric_prefix">SI</a> prefix
1094      * (GB, MB, kB) and using the pattern <code>###0.#</code> by default.
1095      *
1096      * @see <a href="https://en.wikipedia.org/wiki/Metric_prefix">https://en.wikipedia.org/wiki/Metric_prefix</a>
1097      * @see <a href="https://en.wikipedia.org/wiki/Binary_prefix">https://en.wikipedia.org/wiki/Binary_prefix</a>
1098      * @see <a
1099      *      href="https://en.wikipedia.org/wiki/Octet_%28computing%29">https://en.wikipedia.org/wiki/Octet_(computing)</a>
1100      */
1101     // CHECKSTYLE_ON: LineLength
1102     static class FileDecimalFormat extends DecimalFormat {
1103         private static final long serialVersionUID = 4062503546523610081L;
1104 
1105         private final I18N i18n;
1106 
1107         private final Locale locale;
1108 
1109         /**
1110          * Default constructor
1111          *
1112          * @param i18n
1113          * @param locale
1114          */
1115         FileDecimalFormat(I18N i18n, Locale locale) {
1116             super("###0.#");
1117 
1118             this.i18n = i18n;
1119             this.locale = locale;
1120         }
1121 
1122         /** {@inheritDoc} */
1123         @Override
1124         public StringBuffer format(long fs, StringBuffer result, FieldPosition fieldPosition) {
1125             if (fs > 1000 * 1000 * 1000) {
1126                 result = super.format((float) fs / (1000 * 1000 * 1000), result, fieldPosition);
1127                 result.append(" ").append(getString("report.dependencies.file.details.column.size.gb"));
1128                 return result;
1129             }
1130 
1131             if (fs > 1000 * 1000) {
1132                 result = super.format((float) fs / (1000 * 1000), result, fieldPosition);
1133                 result.append(" ").append(getString("report.dependencies.file.details.column.size.mb"));
1134                 return result;
1135             }
1136 
1137             result = super.format((float) fs / (1000), result, fieldPosition);
1138             result.append(" ").append(getString("report.dependencies.file.details.column.size.kb"));
1139             return result;
1140         }
1141 
1142         private String getString(String key) {
1143             return i18n.getString("project-info-reports", locale, key);
1144         }
1145     }
1146 
1147     /**
1148      * Combine total and total by scope in a cell.
1149      */
1150     static class TotalCell {
1151         static final int SCOPES_COUNT = 5;
1152 
1153         final DecimalFormat decimalFormat;
1154 
1155         long total = 0;
1156 
1157         long totalCompileScope = 0;
1158 
1159         long totalTestScope = 0;
1160 
1161         long totalRuntimeScope = 0;
1162 
1163         long totalProvidedScope = 0;
1164 
1165         long totalSystemScope = 0;
1166 
1167         TotalCell(DecimalFormat decimalFormat) {
1168             this.decimalFormat = decimalFormat;
1169         }
1170 
1171         void incrementTotal(String scope) {
1172             addTotal(1, scope);
1173         }
1174 
1175         static String getScope(int index) {
1176             switch (index) {
1177                 case 0:
1178                     return Artifact.SCOPE_COMPILE;
1179                 case 1:
1180                     return Artifact.SCOPE_TEST;
1181                 case 2:
1182                     return Artifact.SCOPE_RUNTIME;
1183                 case 3:
1184                     return Artifact.SCOPE_PROVIDED;
1185                 case 4:
1186                     return Artifact.SCOPE_SYSTEM;
1187                 default:
1188                     return null;
1189             }
1190         }
1191 
1192         long getTotal(int index) {
1193             switch (index) {
1194                 case 0:
1195                     return totalCompileScope;
1196                 case 1:
1197                     return totalTestScope;
1198                 case 2:
1199                     return totalRuntimeScope;
1200                 case 3:
1201                     return totalProvidedScope;
1202                 case 4:
1203                     return totalSystemScope;
1204                 default:
1205                     return total;
1206             }
1207         }
1208 
1209         String getTotalString(int index) {
1210             long totalString = getTotal(index);
1211 
1212             if (totalString <= 0) {
1213                 return "";
1214             }
1215 
1216             StringBuilder sb = new StringBuilder();
1217             if (index >= 0) {
1218                 sb.append(getScope(index)).append(": ");
1219             }
1220             sb.append(decimalFormat.format(getTotal(index)));
1221             return sb.toString();
1222         }
1223 
1224         void addTotal(long add, String scope) {
1225             total += add;
1226 
1227             if (Artifact.SCOPE_COMPILE.equals(scope)) {
1228                 totalCompileScope += add;
1229             } else if (Artifact.SCOPE_TEST.equals(scope)) {
1230                 totalTestScope += add;
1231             } else if (Artifact.SCOPE_RUNTIME.equals(scope)) {
1232                 totalRuntimeScope += add;
1233             } else if (Artifact.SCOPE_PROVIDED.equals(scope)) {
1234                 totalProvidedScope += add;
1235             } else if (Artifact.SCOPE_SYSTEM.equals(scope)) {
1236                 totalSystemScope += add;
1237             }
1238         }
1239 
1240         /** {@inheritDoc} */
1241         public String toString() {
1242             StringBuilder sb = new StringBuilder();
1243             sb.append(decimalFormat.format(total));
1244             sb.append(" (");
1245 
1246             boolean needSeparator = false;
1247             for (int i = 0; i < SCOPES_COUNT; i++) {
1248                 if (getTotal(i) > 0) {
1249                     if (needSeparator) {
1250                         sb.append(", ");
1251                     }
1252                     sb.append(getTotalString(i));
1253                     needSeparator = true;
1254                 }
1255             }
1256 
1257             sb.append(")");
1258 
1259             return sb.toString();
1260         }
1261     }
1262 }