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         if ( !Artifact.SCOPE_SYSTEM.equals( artifact.getScope() ) )
893         {
894             try
895             {
896                 MavenProject artifactProject = repoUtils.getMavenProjectFromRepository( artifact );
897                 String artifactDescription = artifactProject.getDescription();
898                 String artifactUrl = artifactProject.getUrl();
899                 String artifactName = artifactProject.getName();
900 
901                 List<License> licenses = artifactProject.getLicenses();
902 
903                 sink.table();
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                 sink.tableCell_();
994                 sink.tableRow_();
995 
996                 sink.table_();
997             }
998             catch ( ProjectBuildingException e )
999             {
1000                 if ( log.isDebugEnabled() )
1001                 {
1002                     log.debug( "Unable to create Maven project from repository for artifact '"
1003                                + artifact.getId() + "'", e );
1004                 }
1005                 else
1006                 {
1007                     log.info( "Unable to create Maven project from repository for artifact '"
1008                               + artifact.getId() + "', for more information run with -X" );
1009                 }
1010             }
1011         }
1012         else
1013         {
1014             sink.table();
1015 
1016             sink.tableRow();
1017             sink.tableHeaderCell();
1018             sink.text( id );
1019             sink.tableHeaderCell_();
1020             sink.tableRow_();
1021 
1022             sink.tableRow();
1023             sink.tableCell();
1024 
1025             sink.paragraph();
1026             sink.bold();
1027             sink.text( getI18nString( "column.description" ) + ": " );
1028             sink.bold_();
1029             sink.text( getI18nString( "index", "nodescription" ) );
1030             sink.paragraph_();
1031 
1032             if ( artifact.getFile() != null )
1033             {
1034                 sink.paragraph();
1035                 sink.bold();
1036                 sink.text( getI18nString( "column.url" ) + ": " );
1037                 sink.bold_();
1038                 sink.text( artifact.getFile().getAbsolutePath() );
1039                 sink.paragraph_();
1040             }
1041 
1042             sink.tableCell_();
1043             sink.tableRow_();
1044 
1045             sink.table_();
1046         }
1047 
1048         sink.rawText( "</div>" );
1049     }
1050 
1051     private void printGroupedLicenses()
1052     {
1053         for ( Map.Entry<String, Object> entry : licenseMap.entrySet() )
1054         {
1055             String licenseName = entry.getKey();
1056             if ( StringUtils.isEmpty( licenseName ) )
1057             {
1058                 licenseName = getI18nString( "unnamed" );
1059             }
1060 
1061             sink.paragraph();
1062             sink.bold();
1063             sink.text( licenseName );
1064             sink.text( ": " );
1065             sink.bold_();
1066 
1067             @SuppressWarnings( "unchecked" )
1068             SortedSet<String> projects = (SortedSet<String>) entry.getValue();
1069 
1070             for ( Iterator<String> iterator = projects.iterator(); iterator.hasNext(); )
1071             {
1072                 String projectName = iterator.next();
1073                 sink.text( projectName );
1074                 if ( iterator.hasNext() )
1075                 {
1076                     sink.text( ", " );
1077                 }
1078             }
1079 
1080             sink.paragraph_();
1081         }
1082     }
1083 
1084     /**
1085      * Resolves all given artifacts with {@link RepositoryUtils}.
1086      *
1087      ** @param artifacts not null
1088      */
1089     private void resolveAtrifacts( List<Artifact> artifacts )
1090     {
1091         for ( Artifact artifact : artifacts )
1092         {
1093             // TODO site:run Why do we need to resolve this...
1094             if ( artifact.getFile() == null )
1095             {
1096                 if ( Artifact.SCOPE_SYSTEM.equals( artifact.getScope() ) )
1097                 {
1098                     // can not resolve system scope artifact file
1099                     continue;
1100                 }
1101 
1102                 try
1103                 {
1104                     repoUtils.resolve( artifact );
1105                 }
1106                 catch ( ArtifactResolverException e )
1107                 {
1108                     log.error( "Artifact " + artifact.getId() + " can't be resolved.", e );
1109                     continue;
1110                 }
1111 
1112                 if ( artifact.getFile() == null )
1113                 {
1114                     log.error( "Artifact " + artifact.getId() + " has no file, even after resolution." );
1115                 }
1116             }
1117         }
1118     }
1119 
1120     /**
1121      * @param artifacts not null
1122      * @return <code>true</code> if one artifact in the list has a classifier, <code>false</code> otherwise.
1123      */
1124     private boolean hasClassifier( List<Artifact> artifacts )
1125     {
1126         for ( Artifact artifact : artifacts )
1127         {
1128             if ( StringUtils.isNotEmpty( artifact.getClassifier() ) )
1129             {
1130                 return true;
1131             }
1132         }
1133 
1134         return false;
1135     }
1136 
1137     /**
1138      * @param artifacts not null
1139      * @return <code>true</code> if one artifact in the list is optional, <code>false</code> otherwise.
1140      */
1141     private boolean hasOptional( List<Artifact> artifacts )
1142     {
1143         for ( Artifact artifact : artifacts )
1144         {
1145             if ( artifact.isOptional() )
1146             {
1147                 return true;
1148             }
1149         }
1150 
1151         return false;
1152     }
1153 
1154     /**
1155      * @param artifacts not null
1156      * @return <code>true</code> if one artifact in the list is sealed, <code>false</code> otherwise.
1157      */
1158     private boolean hasSealed( List<Artifact> artifacts )
1159     {
1160         for ( Artifact artifact : artifacts )
1161         {
1162             if ( artifact.getFile() != null && JAR_SUBTYPE.contains( artifact.getType().toLowerCase() ) )
1163             {
1164                 try
1165                 {
1166                     JarData jarDetails = dependencies.getJarDependencyDetails( artifact );
1167                     if ( jarDetails.isSealed() )
1168                     {
1169                         return true;
1170                     }
1171                 }
1172                 catch ( IOException e )
1173                 {
1174                     log.error( "Artifact " + artifact.getId() + " caused IOException: " + e.getMessage(), e );
1175                 }
1176             }
1177         }
1178         return false;
1179     }
1180 
1181     // CHECKSTYLE_OFF: LineLength
1182     /**
1183      * Formats file length with the associated <a href="https://en.wikipedia.org/wiki/Metric_prefix">SI</a> prefix
1184      * (GB, MB, kB) and using the pattern <code>###0.#</code> by default.
1185      *
1186      * @see <a href="https://en.wikipedia.org/wiki/Metric_prefix">https://en.wikipedia.org/wiki/Metric_prefix</a>
1187      * @see <a href="https://en.wikipedia.org/wiki/Binary_prefix">https://en.wikipedia.org/wiki/Binary_prefix</a>
1188      * @see <a
1189      *      href="https://en.wikipedia.org/wiki/Octet_%28computing%29">https://en.wikipedia.org/wiki/Octet_(computing)</a>
1190      */
1191     // CHECKSTYLE_ON: LineLength
1192     static class FileDecimalFormat
1193         extends DecimalFormat
1194     {
1195         private static final long serialVersionUID = 4062503546523610081L;
1196 
1197         private final I18N i18n;
1198 
1199         private final Locale locale;
1200 
1201         /**
1202          * Default constructor
1203          *
1204          * @param i18n
1205          * @param locale
1206          */
1207         FileDecimalFormat( I18N i18n, Locale locale )
1208         {
1209             super( "###0.#" );
1210 
1211             this.i18n = i18n;
1212             this.locale = locale;
1213         }
1214 
1215         /** {@inheritDoc} */
1216         @Override
1217         public StringBuffer format( long fs, StringBuffer result, FieldPosition fieldPosition )
1218         {
1219             if ( fs > 1000 * 1000 * 1000 )
1220             {
1221                 result = super.format( (float) fs / ( 1000 * 1000 * 1000 ), result, fieldPosition );
1222                 result.append( " " ).append( getString( "report.dependencies.file.details.column.size.gb" ) );
1223                 return result;
1224             }
1225 
1226             if ( fs > 1000 * 1000 )
1227             {
1228                 result = super.format( (float) fs / ( 1000 * 1000 ), result, fieldPosition );
1229                 result.append( " " ).append( getString( "report.dependencies.file.details.column.size.mb" ) );
1230                 return result;
1231             }
1232 
1233             result = super.format( (float) fs / ( 1000 ), result, fieldPosition );
1234             result.append( " " ).append( getString( "report.dependencies.file.details.column.size.kb" ) );
1235             return result;
1236         }
1237 
1238         private String getString( String key )
1239         {
1240             return i18n.getString( "project-info-reports", locale, key );
1241         }
1242     }
1243 
1244     /**
1245      * Combine total and total by scope in a cell.
1246      */
1247     static class TotalCell
1248     {
1249         static final int SCOPES_COUNT = 5;
1250 
1251         final DecimalFormat decimalFormat;
1252 
1253         long total = 0;
1254 
1255         long totalCompileScope = 0;
1256 
1257         long totalTestScope = 0;
1258 
1259         long totalRuntimeScope = 0;
1260 
1261         long totalProvidedScope = 0;
1262 
1263         long totalSystemScope = 0;
1264 
1265         TotalCell( DecimalFormat decimalFormat )
1266         {
1267             this.decimalFormat = decimalFormat;
1268         }
1269 
1270         void incrementTotal( String scope )
1271         {
1272             addTotal( 1, scope );
1273         }
1274 
1275         static String getScope( int index )
1276         {
1277             switch ( index )
1278             {
1279                 case 0:
1280                     return Artifact.SCOPE_COMPILE;
1281                 case 1:
1282                     return Artifact.SCOPE_TEST;
1283                 case 2:
1284                     return Artifact.SCOPE_RUNTIME;
1285                 case 3:
1286                     return Artifact.SCOPE_PROVIDED;
1287                 case 4:
1288                     return Artifact.SCOPE_SYSTEM;
1289                 default:
1290                     return null;
1291             }
1292         }
1293 
1294         long getTotal( int index )
1295         {
1296             switch ( index )
1297             {
1298                 case 0:
1299                     return totalCompileScope;
1300                 case 1:
1301                     return totalTestScope;
1302                 case 2:
1303                     return totalRuntimeScope;
1304                 case 3:
1305                     return totalProvidedScope;
1306                 case 4:
1307                     return totalSystemScope;
1308                 default:
1309                     return total;
1310             }
1311         }
1312 
1313         String getTotalString( int index )
1314         {
1315             long totalString = getTotal( index );
1316 
1317             if ( totalString <= 0 )
1318             {
1319                 return "";
1320             }
1321 
1322             StringBuilder sb = new StringBuilder();
1323             if ( index >= 0 )
1324             {
1325                 sb.append( getScope( index ) ).append( ": " );
1326             }
1327             sb.append( decimalFormat.format( getTotal( index ) ) );
1328             return sb.toString();
1329         }
1330 
1331         void addTotal( long add, String scope )
1332         {
1333             total += add;
1334 
1335             if ( Artifact.SCOPE_COMPILE.equals( scope ) )
1336             {
1337                 totalCompileScope += add;
1338             }
1339             else if ( Artifact.SCOPE_TEST.equals( scope ) )
1340             {
1341                 totalTestScope += add;
1342             }
1343             else if ( Artifact.SCOPE_RUNTIME.equals( scope ) )
1344             {
1345                 totalRuntimeScope += add;
1346             }
1347             else if ( Artifact.SCOPE_PROVIDED.equals( scope ) )
1348             {
1349                 totalProvidedScope += add;
1350             }
1351             else if ( Artifact.SCOPE_SYSTEM.equals( scope ) )
1352             {
1353                 totalSystemScope += add;
1354             }
1355         }
1356 
1357         /** {@inheritDoc} */
1358         public String toString()
1359         {
1360             StringBuilder sb = new StringBuilder();
1361             sb.append( decimalFormat.format( total ) );
1362             sb.append( " (" );
1363 
1364             boolean needSeparator = false;
1365             for ( int i = 0; i < SCOPES_COUNT; i++ )
1366             {
1367                 if ( getTotal( i ) > 0 )
1368                 {
1369                     if ( needSeparator )
1370                     {
1371                         sb.append( ", " );
1372                     }
1373                     sb.append( getTotalString( i ) );
1374                     needSeparator = true;
1375                 }
1376             }
1377 
1378             sb.append( ")" );
1379 
1380             return sb.toString();
1381         }
1382     }
1383 }