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