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