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