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