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.lang.reflect.InvocationTargetException;
25  import java.net.URL;
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.commons.lang.SystemUtils;
43  import org.apache.maven.artifact.Artifact;
44  import org.apache.maven.artifact.factory.ArtifactFactory;
45  import org.apache.maven.artifact.repository.ArtifactRepository;
46  import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy;
47  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
48  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
49  import org.apache.maven.doxia.sink.Sink;
50  import org.apache.maven.doxia.sink.SinkEventAttributeSet;
51  import org.apache.maven.doxia.sink.SinkEventAttributes;
52  import org.apache.maven.doxia.util.HtmlTools;
53  import org.apache.maven.model.License;
54  import org.apache.maven.plugin.logging.Log;
55  import org.apache.maven.project.MavenProject;
56  import org.apache.maven.project.MavenProjectBuilder;
57  import org.apache.maven.project.ProjectBuildingException;
58  import org.apache.maven.report.projectinfo.AbstractProjectInfoRenderer;
59  import org.apache.maven.report.projectinfo.ProjectInfoReportUtils;
60  import org.apache.maven.report.projectinfo.dependencies.Dependencies;
61  import org.apache.maven.report.projectinfo.dependencies.DependenciesReportConfiguration;
62  import org.apache.maven.report.projectinfo.dependencies.RepositoryUtils;
63  import org.apache.maven.settings.Settings;
64  import org.apache.maven.shared.dependency.graph.DependencyNode;
65  import org.apache.maven.shared.jar.JarData;
66  import org.codehaus.plexus.i18n.I18N;
67  import org.codehaus.plexus.util.StringUtils;
68  
69  /**
70   * Renderer the dependencies report.
71   *
72   * @version $Id: DependenciesRenderer.html 964486 2015-09-05 21:32:50Z hboutemy $
73   * @since 2.1
74   */
75  public class DependenciesRenderer
76      extends AbstractProjectInfoRenderer
77  {
78      /** URL for the 'icon_info_sml.gif' image */
79      private static final String IMG_INFO_URL = "./images/icon_info_sml.gif";
80  
81      /** URL for the 'close.gif' image */
82      private static final String IMG_CLOSE_URL = "./images/close.gif";
83  
84      /** Used to format decimal values in the "Dependency File Details" table */
85      protected static final DecimalFormat DEFAULT_DECIMAL_FORMAT = new DecimalFormat( "###0" );
86  
87      private static final Set<String> JAR_SUBTYPE;
88  
89      /**
90       * An HTML script tag with the Javascript used by the dependencies report.
91       */
92      private static final String JAVASCRIPT;
93  
94      private final DependencyNode dependencyNode;
95  
96      private final Dependencies dependencies;
97  
98      private final DependenciesReportConfiguration configuration;
99  
100     private final Log log;
101 
102     private final Settings settings;
103 
104     private final RepositoryUtils repoUtils;
105 
106     /** Used to format file length values */
107     private final DecimalFormat fileLengthDecimalFormat;
108 
109     /**
110      * @since 2.1.1
111      */
112     private int section;
113 
114     /** Counter for unique IDs that is consistent across generations. */
115     private int idCounter = 0;
116 
117     /**
118      * Will be filled with license name / set of projects.
119      */
120     private Map<String, Object> licenseMap = new HashMap<String, Object>()
121     {
122         private static final long serialVersionUID = 1L;
123 
124         /** {@inheritDoc} */
125         public Object put( String key, Object value )
126         {
127             // handle multiple values as a set to avoid duplicates
128             @SuppressWarnings( "unchecked" )
129             SortedSet<Object> valueList = (SortedSet<Object>) get( key );
130             if ( valueList == null )
131             {
132                 valueList = new TreeSet<Object>();
133             }
134             valueList.add( value );
135             return super.put( key, valueList );
136         }
137     };
138 
139     private final ArtifactFactory artifactFactory;
140 
141     private final MavenProjectBuilder mavenProjectBuilder;
142 
143     private final List<ArtifactRepository> remoteRepositories;
144 
145     private final ArtifactRepository localRepository;
146 
147     static
148     {
149         Set<String> jarSubtype = new HashSet<String>();
150         jarSubtype.add( "jar" );
151         jarSubtype.add( "war" );
152         jarSubtype.add( "ear" );
153         jarSubtype.add( "sar" );
154         jarSubtype.add( "rar" );
155         jarSubtype.add( "par" );
156         jarSubtype.add( "ejb" );
157         JAR_SUBTYPE = Collections.unmodifiableSet( jarSubtype );
158 
159         JAVASCRIPT =
160             "<script language=\"javascript\" type=\"text/javascript\">" + SystemUtils.LINE_SEPARATOR
161                 + "      function toggleDependencyDetail( divId, imgId )" + SystemUtils.LINE_SEPARATOR + "      {"
162                 + SystemUtils.LINE_SEPARATOR + "        var div = document.getElementById( divId );"
163                 + SystemUtils.LINE_SEPARATOR + "        var img = document.getElementById( imgId );"
164                 + SystemUtils.LINE_SEPARATOR + "        if( div.style.display == '' )" + SystemUtils.LINE_SEPARATOR
165                 + "        {" + SystemUtils.LINE_SEPARATOR + "          div.style.display = 'none';"
166                 + SystemUtils.LINE_SEPARATOR + "          img.src='" + IMG_INFO_URL + "';" + SystemUtils.LINE_SEPARATOR
167                 + "        }" + SystemUtils.LINE_SEPARATOR + "        else" + SystemUtils.LINE_SEPARATOR + "        {"
168                 + SystemUtils.LINE_SEPARATOR + "          div.style.display = '';" + SystemUtils.LINE_SEPARATOR
169                 + "          img.src='" + IMG_CLOSE_URL + "';" + SystemUtils.LINE_SEPARATOR + "        }"
170                 + SystemUtils.LINE_SEPARATOR + "      }" + SystemUtils.LINE_SEPARATOR + "</script>"
171                 + SystemUtils.LINE_SEPARATOR;
172     }
173 
174     /**
175      * Default constructor.
176      *
177      * @param sink {@link Sink}
178      * @param locale {@link Locale}
179      * @param i18n {@link I18N}
180      * @param log {@link Log}
181      * @param settings {@link Settings}
182      * @param dependencies {@link Dependencies}
183      * @param dependencyTreeNode {@link DependencyNode}
184      * @param config {@link DependenciesReportConfiguration}
185      * @param repoUtils {@link RepositoryUtils}
186      * @param artifactFactory {@link ArtifactFactory}
187      * @param mavenProjectBuilder {@link MavenProjectBuilder}
188      * @param remoteRepositories {@link ArtifactRepository}
189      * @param localRepository {@link ArtifactRepository}
190      */
191     public DependenciesRenderer( Sink sink, Locale locale, I18N i18n, Log log, Settings settings,
192                                  Dependencies dependencies, DependencyNode dependencyTreeNode,
193                                  DependenciesReportConfiguration config, RepositoryUtils repoUtils,
194                                  ArtifactFactory artifactFactory, MavenProjectBuilder mavenProjectBuilder,
195                                  List<ArtifactRepository> remoteRepositories, ArtifactRepository localRepository )
196     {
197         super( sink, i18n, locale );
198 
199         this.log = log;
200         this.settings = settings;
201         this.dependencies = dependencies;
202         this.dependencyNode = dependencyTreeNode;
203         this.repoUtils = repoUtils;
204         this.configuration = config;
205         this.artifactFactory = artifactFactory;
206         this.mavenProjectBuilder = mavenProjectBuilder;
207         this.remoteRepositories = remoteRepositories;
208         this.localRepository = localRepository;
209 
210         // Using the right set of symbols depending of the locale
211         DEFAULT_DECIMAL_FORMAT.setDecimalFormatSymbols( new DecimalFormatSymbols( locale ) );
212 
213         this.fileLengthDecimalFormat = new FileDecimalFormat( i18n, locale );
214         this.fileLengthDecimalFormat.setDecimalFormatSymbols( new DecimalFormatSymbols( locale ) );
215     }
216 
217     @Override
218     protected String getI18Nsection()
219     {
220         return "dependencies";
221     }
222 
223     // ----------------------------------------------------------------------
224     // Public methods
225     // ----------------------------------------------------------------------
226 
227     @Override
228     public void renderBody()
229     {
230         // Dependencies report
231 
232         if ( !dependencies.hasDependencies() )
233         {
234             startSection( getTitle() );
235 
236             paragraph( getI18nString( "nolist" ) );
237 
238             endSection();
239 
240             return;
241         }
242 
243         // === Section: Project Dependencies.
244         renderSectionProjectDependencies();
245 
246         // === Section: Project Transitive Dependencies.
247         renderSectionProjectTransitiveDependencies();
248 
249         // === Section: Project Dependency Graph.
250         renderSectionProjectDependencyGraph();
251 
252         // === Section: Licenses
253         renderSectionDependencyLicenseListing();
254 
255         if ( configuration.getDependencyDetailsEnabled() )
256         {
257             // === Section: Dependency File Details.
258             renderSectionDependencyFileDetails();
259         }
260 
261         if ( configuration.getDependencyLocationsEnabled() )
262         {
263             // === Section: Dependency Repository Locations.
264             renderSectionDependencyRepositoryLocations();
265         }
266     }
267 
268     // ----------------------------------------------------------------------
269     // Protected methods
270     // ----------------------------------------------------------------------
271 
272     /** {@inheritDoc} */
273     // workaround for MPIR-140
274     // TODO Remove me when MSHARED-390 has been resolved
275     protected void startSection( String name )
276     {
277         startSection( name, name );
278     }
279 
280     /**
281      * Start section with a name and a specific anchor.
282      *
283      * @param anchor not null
284      * @param name not null
285      */
286     // TODO Remove me when MSHARED-390 has been resolved
287     protected void startSection( String anchor, String name )
288     {
289         section = section + 1;
290 
291         super.sink.anchor( HtmlTools.encodeId( anchor ) );
292         super.sink.anchor_();
293 
294         switch ( section )
295         {
296             case 1:
297                 sink.section1();
298                 sink.sectionTitle1();
299                 break;
300             case 2:
301                 sink.section2();
302                 sink.sectionTitle2();
303                 break;
304             case 3:
305                 sink.section3();
306                 sink.sectionTitle3();
307                 break;
308             case 4:
309                 sink.section4();
310                 sink.sectionTitle4();
311                 break;
312             case 5:
313                 sink.section5();
314                 sink.sectionTitle5();
315                 break;
316 
317             default:
318                 // TODO: warning - just don't start a section
319                 break;
320         }
321 
322         text( name );
323 
324         switch ( section )
325         {
326             case 1:
327                 sink.sectionTitle1_();
328                 break;
329             case 2:
330                 sink.sectionTitle2_();
331                 break;
332             case 3:
333                 sink.sectionTitle3_();
334                 break;
335             case 4:
336                 sink.sectionTitle4_();
337                 break;
338             case 5:
339                 sink.sectionTitle5_();
340                 break;
341 
342             default:
343                 // TODO: warning - just don't start a section
344                 break;
345         }
346     }
347 
348     /** {@inheritDoc} */
349     // workaround for MPIR-140
350     // TODO Remove me when MSHARED-390 has been resolved
351     protected void endSection()
352     {
353         switch ( section )
354         {
355             case 1:
356                 sink.section1_();
357                 break;
358             case 2:
359                 sink.section2_();
360                 break;
361             case 3:
362                 sink.section3_();
363                 break;
364             case 4:
365                 sink.section4_();
366                 break;
367             case 5:
368                 sink.section5_();
369                 break;
370 
371             default:
372                 // TODO: warning - just don't start a section
373                 break;
374         }
375 
376         section = section - 1;
377 
378         if ( section < 0 )
379         {
380             throw new IllegalStateException( "Too many closing sections" );
381         }
382     }
383 
384     // ----------------------------------------------------------------------
385     // Private methods
386     // ----------------------------------------------------------------------
387 
388     /**
389      * @param withClassifier <code>true</code> to include the classifier column, <code>false</code> otherwise.
390      * @param withOptional <code>true</code> to include the optional column, <code>false</code> otherwise.
391      * @return the dependency table header with/without classifier/optional column
392      * @see #renderArtifactRow(Artifact, boolean, boolean)
393      */
394     private String[] getDependencyTableHeader( boolean withClassifier, boolean withOptional )
395     {
396         String groupId = getI18nString( "column.groupId" );
397         String artifactId = getI18nString( "column.artifactId" );
398         String version = getI18nString( "column.version" );
399         String classifier = getI18nString( "column.classifier" );
400         String type = getI18nString( "column.type" );
401         String license = getI18nString( "column.license" );
402         String optional = getI18nString( "column.optional" );
403 
404         if ( withClassifier )
405         {
406             if ( withOptional )
407             {
408                 return new String[] { groupId, artifactId, version, classifier, type, license, optional };
409             }
410 
411             return new String[] { groupId, artifactId, version, classifier, type, license };
412         }
413 
414         if ( withOptional )
415         {
416             return new String[] { groupId, artifactId, version, type, license, optional };
417         }
418 
419         return new String[] { groupId, artifactId, version, type, license };
420     }
421 
422     private void renderSectionProjectDependencies()
423     {
424         startSection( getTitle() );
425 
426         // collect dependencies by scope
427         Map<String, List<Artifact>> dependenciesByScope = dependencies.getDependenciesByScope( false );
428 
429         renderDependenciesForAllScopes( dependenciesByScope, false );
430 
431         endSection();
432     }
433 
434     /**
435      * @param dependenciesByScope map with supported scopes as key and a list of <code>Artifact</code> as values.
436      * @param isTransitive <code>true</code> if it is transitive dependencies rendering.
437      * @see Artifact#SCOPE_COMPILE
438      * @see Artifact#SCOPE_PROVIDED
439      * @see Artifact#SCOPE_RUNTIME
440      * @see Artifact#SCOPE_SYSTEM
441      * @see Artifact#SCOPE_TEST
442      */
443     private void renderDependenciesForAllScopes( Map<String, List<Artifact>> dependenciesByScope, boolean isTransitive )
444     {
445         renderDependenciesForScope( Artifact.SCOPE_COMPILE, dependenciesByScope.get( Artifact.SCOPE_COMPILE ),
446                                     isTransitive );
447         renderDependenciesForScope( Artifact.SCOPE_RUNTIME, dependenciesByScope.get( Artifact.SCOPE_RUNTIME ),
448                                     isTransitive );
449         renderDependenciesForScope( Artifact.SCOPE_TEST, dependenciesByScope.get( Artifact.SCOPE_TEST ), isTransitive );
450         renderDependenciesForScope( Artifact.SCOPE_PROVIDED, dependenciesByScope.get( Artifact.SCOPE_PROVIDED ),
451                                     isTransitive );
452         renderDependenciesForScope( Artifact.SCOPE_SYSTEM, dependenciesByScope.get( Artifact.SCOPE_SYSTEM ),
453                                     isTransitive );
454     }
455 
456     private void renderSectionProjectTransitiveDependencies()
457     {
458         Map<String, List<Artifact>> dependenciesByScope = dependencies.getDependenciesByScope( true );
459 
460         startSection( getI18nString( "transitive.title" ) );
461 
462         if ( dependenciesByScope.values().isEmpty() )
463         {
464             paragraph( getI18nString( "transitive.nolist" ) );
465         }
466         else
467         {
468             paragraph( getI18nString( "transitive.intro" ) );
469 
470             renderDependenciesForAllScopes( dependenciesByScope, true );
471         }
472 
473         endSection();
474     }
475 
476     private void renderSectionProjectDependencyGraph()
477     {
478         startSection( getI18nString( "graph.title" ) );
479 
480         // === SubSection: Dependency Tree
481         renderSectionDependencyTree();
482 
483         endSection();
484     }
485 
486     private void renderSectionDependencyTree()
487     {
488         sink.rawText( JAVASCRIPT );
489 
490         // for Dependencies Graph Tree
491         startSection( getI18nString( "graph.tree.title" ) );
492 
493         sink.list();
494         printDependencyListing( dependencyNode );
495         sink.list_();
496 
497         endSection();
498     }
499 
500     private void renderSectionDependencyFileDetails()
501     {
502         startSection( getI18nString( "file.details.title" ) );
503 
504         List<Artifact> alldeps = dependencies.getAllDependencies();
505         Collections.sort( alldeps, getArtifactComparator() );
506 
507         // i18n
508         String filename = getI18nString( "file.details.column.file" );
509         String size = getI18nString( "file.details.column.size" );
510         String entries = getI18nString( "file.details.column.entries" );
511         String classes = getI18nString( "file.details.column.classes" );
512         String packages = getI18nString( "file.details.column.packages" );
513         String javaVersion = getI18nString( "file.details.column.javaVersion" );
514         String debugInformation = getI18nString( "file.details.column.debuginformation" );
515         String debugInformationTitle = getI18nString( "file.details.columntitle.debuginformation" );
516         String debugInformationCellYes = getI18nString( "file.details.cell.debuginformation.yes" );
517         String debugInformationCellNo = getI18nString( "file.details.cell.debuginformation.no" );
518         String sealed = getI18nString( "file.details.column.sealed" );
519 
520         int[] justification =
521             new int[] { Sink.JUSTIFY_LEFT, Sink.JUSTIFY_RIGHT, Sink.JUSTIFY_RIGHT, Sink.JUSTIFY_RIGHT,
522                 Sink.JUSTIFY_RIGHT, Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER };
523 
524         startTable( justification, false );
525 
526         TotalCell totaldeps = new TotalCell( DEFAULT_DECIMAL_FORMAT );
527         TotalCell totaldepsize = new TotalCell( fileLengthDecimalFormat );
528         TotalCell totalentries = new TotalCell( DEFAULT_DECIMAL_FORMAT );
529         TotalCell totalclasses = new TotalCell( DEFAULT_DECIMAL_FORMAT );
530         TotalCell totalpackages = new TotalCell( DEFAULT_DECIMAL_FORMAT );
531         double highestJavaVersion = 0.0;
532         TotalCell totalDebugInformation = new TotalCell( DEFAULT_DECIMAL_FORMAT );
533         TotalCell totalsealed = new TotalCell( DEFAULT_DECIMAL_FORMAT );
534 
535         boolean hasSealed = hasSealed( alldeps );
536 
537         // Table header
538         String[] tableHeader;
539         String[] tableHeaderTitles;
540         if ( hasSealed )
541         {
542             tableHeader = new String[] { filename, size, entries, classes, packages, javaVersion, debugInformation,
543                                          sealed };
544             tableHeaderTitles = new String[] { null, null, null, null, null, null, debugInformationTitle, null };
545         }
546         else
547         {
548             tableHeader = new String[] { filename, size, entries, classes, packages, javaVersion, debugInformation };
549             tableHeaderTitles = new String[] { null, null, null, null, null, null, debugInformationTitle };
550         }
551         tableHeader( tableHeader, tableHeaderTitles );
552 
553         // Table rows
554         for ( Artifact artifact : alldeps )
555         {
556             if ( artifact.getFile() == null )
557             {
558                 log.warn( "Artifact " + artifact.getId() + " has no file,"
559                     + " won't be listed in dependency files details." );
560                 continue;
561             }
562 
563             File artifactFile = dependencies.getFile( artifact );
564 
565             totaldeps.incrementTotal( artifact.getScope() );
566             totaldepsize.addTotal( artifactFile.length(), artifact.getScope() );
567 
568             if ( JAR_SUBTYPE.contains( artifact.getType().toLowerCase() ) )
569             {
570                 try
571                 {
572                     JarData jarDetails = dependencies.getJarDependencyDetails( artifact );
573 
574                     String debugInformationCellValue = debugInformationCellNo;
575                     if ( jarDetails.isDebugPresent() )
576                     {
577                         debugInformationCellValue = debugInformationCellYes;
578                         totalDebugInformation.incrementTotal( artifact.getScope() );
579                     }
580 
581                     totalentries.addTotal( jarDetails.getNumEntries(), artifact.getScope() );
582                     totalclasses.addTotal( jarDetails.getNumClasses(), artifact.getScope() );
583                     totalpackages.addTotal( jarDetails.getNumPackages(), artifact.getScope() );
584 
585                     try
586                     {
587                         if ( jarDetails.getJdkRevision() != null )
588                         {
589                             highestJavaVersion = Math.max( highestJavaVersion,
590                                                      Double.parseDouble( jarDetails.getJdkRevision() ) );
591                         }
592                     }
593                     catch ( NumberFormatException e )
594                     {
595                         // ignore
596                     }
597 
598                     String sealedstr = "";
599                     if ( jarDetails.isSealed() )
600                     {
601                         sealedstr = "sealed";
602                         totalsealed.incrementTotal( artifact.getScope() );
603                     }
604 
605                     String name = artifactFile.getName();
606                     String fileLength = fileLengthDecimalFormat.format( artifactFile.length() );
607 
608                     if ( artifactFile.isDirectory() )
609                     {
610                         File parent = artifactFile.getParentFile();
611                         name = parent.getParentFile().getName() + '/' + parent.getName() + '/' + artifactFile.getName();
612                         fileLength = "-";
613                     }
614 
615                     tableRow( hasSealed,
616                               new String[] { name, fileLength,
617                                   DEFAULT_DECIMAL_FORMAT.format( jarDetails.getNumEntries() ),
618                                   DEFAULT_DECIMAL_FORMAT.format( jarDetails.getNumClasses() ),
619                                   DEFAULT_DECIMAL_FORMAT.format( jarDetails.getNumPackages() ),
620                                   jarDetails.getJdkRevision(), debugInformationCellValue, sealedstr } );
621                 }
622                 catch ( IOException e )
623                 {
624                     createExceptionInfoTableRow( artifact, artifactFile, e, hasSealed );
625                 }
626             }
627             else
628             {
629                 tableRow( hasSealed,
630                           new String[] { artifactFile.getName(),
631                               fileLengthDecimalFormat.format( artifactFile.length() ), "", "", "", "", "", "" } );
632             }
633         }
634 
635         // Total raws
636         tableHeader[0] = getI18nString( "file.details.total" );
637         tableHeader( tableHeader );
638 
639         justification[0] = Sink.JUSTIFY_RIGHT;
640         justification[6] = Sink.JUSTIFY_RIGHT;
641 
642         for ( int i = -1; i < TotalCell.SCOPES_COUNT; i++ )
643         {
644             if ( totaldeps.getTotal( i ) > 0 )
645             {
646                 tableRow( hasSealed,
647                           new String[] { totaldeps.getTotalString( i ), totaldepsize.getTotalString( i ),
648                               totalentries.getTotalString( i ), totalclasses.getTotalString( i ),
649                               totalpackages.getTotalString( i ), ( i < 0 ) ? String.valueOf( highestJavaVersion ) : "",
650                               totalDebugInformation.getTotalString( i ), totalsealed.getTotalString( i ) } );
651             }
652         }
653 
654         endTable();
655         endSection();
656     }
657 
658     // Almost as same as in the abstract class but includes the title attribute
659     private void tableHeader( String[] content, String[] titles )
660     {
661         sink.tableRow();
662 
663         if ( content != null )
664         {
665             if ( titles != null && content.length != titles.length )
666             {
667                 // CHECKSTYLE_OFF: LineLength
668                 throw new IllegalArgumentException( "Length of title array must equal the length of the content array" );
669                 // CHECKSTYLE_ON: LineLength
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             SinkEventAttributes attributes = new SinkEventAttributeSet( SinkEventAttributes.TITLE, title );
693             sink.tableHeaderCell( attributes );
694         }
695         else
696         {
697             sink.tableHeaderCell();
698         }
699 
700         text( text );
701 
702         sink.tableHeaderCell_();
703     }
704 
705     private void tableRow( boolean fullRow, String[] content )
706     {
707         sink.tableRow();
708 
709         int count = fullRow ? content.length : ( content.length - 1 );
710 
711         for ( int i = 0; i < count; i++ )
712         {
713             tableCell( content[i] );
714         }
715 
716         sink.tableRow_();
717     }
718 
719     private void createExceptionInfoTableRow( Artifact artifact, File artifactFile, Exception e, boolean hasSealed )
720     {
721         tableRow( hasSealed, new String[] { artifact.getId(), artifactFile.getAbsolutePath(), e.getMessage(), "", "",
722             "", "", "" } );
723     }
724 
725     private void populateRepositoryMap( Map<String, ArtifactRepository> repos, List<ArtifactRepository> rowRepos )
726     {
727         for ( ArtifactRepository repo : rowRepos )
728         {
729             repos.put( repo.getId(), repo );
730         }
731     }
732 
733     private void blacklistRepositoryMap( Map<String, ArtifactRepository> repos, List<String> repoUrlBlackListed )
734     {
735         for ( ArtifactRepository repo : repos.values() )
736         {
737             // ping repo
738             if ( repo.isBlacklisted() )
739             {
740                 repoUrlBlackListed.add( repo.getUrl() );
741             }
742             else
743             {
744                 if ( repoUrlBlackListed.contains( repo.getUrl() ) )
745                 {
746                     repo.setBlacklisted( true );
747                 }
748                 else
749                 {
750                     try
751                     {
752                         URL repoUrl = new URL( repo.getUrl() );
753                         if ( ProjectInfoReportUtils.getContent( repoUrl, settings ) == null )
754                         {
755                             log.warn( "The repository url '" + repoUrl + "' has no stream - Repository '"
756                                 + repo.getId() + "' will be blacklisted." );
757                             repo.setBlacklisted( true );
758                             repoUrlBlackListed.add( repo.getUrl() );
759                         }
760                     }
761                     catch ( IOException e )
762                     {
763                         log.warn( "The repository url '" + repo.getUrl() + "' is invalid - Repository '" + repo.getId()
764                             + "' will be blacklisted." );
765                         repo.setBlacklisted( true );
766                         repoUrlBlackListed.add( repo.getUrl() );
767                     }
768                 }
769             }
770         }
771     }
772 
773     @SuppressWarnings( "unchecked" )
774     private void renderSectionDependencyRepositoryLocations()
775     {
776         startSection( getI18nString( "repo.locations.title" ) );
777 
778         // Collect Alphabetical Dependencies
779         List<Artifact> alldeps = dependencies.getAllDependencies();
780         Collections.sort( alldeps, getArtifactComparator() );
781 
782         // Collect Repositories
783         Map<String, ArtifactRepository> repoMap = new HashMap<String, ArtifactRepository>();
784 
785         populateRepositoryMap( repoMap, repoUtils.getRemoteArtifactRepositories() );
786         for ( Artifact artifact : alldeps )
787         {
788             try
789             {
790                 MavenProject artifactProject = repoUtils.getMavenProjectFromRepository( artifact );
791                 populateRepositoryMap( repoMap, artifactProject.getRemoteArtifactRepositories() );
792             }
793             catch ( ProjectBuildingException e )
794             {
795                 log.warn( "Unable to create Maven project from repository for artifact " + artifact.getId(), e );
796             }
797         }
798 
799         List<String> repoUrlBlackListed = new ArrayList<String>();
800         blacklistRepositoryMap( repoMap, repoUrlBlackListed );
801 
802         // Render Repository List
803 
804         printRepositories( repoMap, repoUrlBlackListed );
805 
806         // Render Artifacts locations
807 
808         printArtifactsLocations( repoMap, repoUrlBlackListed, alldeps );
809 
810         endSection();
811     }
812 
813     private void renderSectionDependencyLicenseListing()
814     {
815         startSection( getI18nString( "graph.tables.licenses" ) );
816         printGroupedLicenses();
817         endSection();
818     }
819 
820     private void renderDependenciesForScope( String scope, List<Artifact> artifacts, boolean isTransitive )
821     {
822         if ( artifacts != null )
823         {
824             boolean withClassifier = hasClassifier( artifacts );
825             boolean withOptional = hasOptional( artifacts );
826             String[] tableHeader = getDependencyTableHeader( withClassifier, withOptional );
827 
828             // can't use straight artifact comparison because we want optional last
829             Collections.sort( artifacts, getArtifactComparator() );
830 
831             String anchorByScope =
832                 ( isTransitive ? getI18nString( "transitive.title" ) + "_" + scope : getI18nString( "title" ) + "_"
833                     + scope );
834             startSection( anchorByScope, scope );
835 
836             paragraph( getI18nString( "intro." + scope ) );
837 
838             startTable();
839             tableHeader( tableHeader );
840             for ( Artifact artifact : artifacts )
841             {
842                 renderArtifactRow( artifact, withClassifier, withOptional );
843             }
844             endTable();
845 
846             endSection();
847         }
848     }
849 
850     private Comparator<Artifact> getArtifactComparator()
851     {
852         return new Comparator<Artifact>()
853         {
854             public int compare( Artifact a1, Artifact a2 )
855             {
856                 // put optional last
857                 if ( a1.isOptional() && !a2.isOptional() )
858                 {
859                     return +1;
860                 }
861                 else if ( !a1.isOptional() && a2.isOptional() )
862                 {
863                     return -1;
864                 }
865                 else
866                 {
867                     return a1.compareTo( a2 );
868                 }
869             }
870         };
871     }
872 
873     /**
874      * @param artifact not null
875      * @param withClassifier <code>true</code> to include the classifier column, <code>false</code> otherwise.
876      * @param withOptional <code>true</code> to include the optional column, <code>false</code> otherwise.
877      * @see #getDependencyTableHeader(boolean, boolean)
878      */
879     private void renderArtifactRow( Artifact artifact, boolean withClassifier, boolean withOptional )
880     {
881         String isOptional =
882             artifact.isOptional() ? getI18nString( "column.isOptional" ) : getI18nString( "column.isNotOptional" );
883 
884         String url =
885             ProjectInfoReportUtils.getArtifactUrl( artifactFactory, artifact, mavenProjectBuilder, remoteRepositories,
886                                                    localRepository );
887         String artifactIdCell = ProjectInfoReportUtils.getArtifactIdCell( artifact.getArtifactId(), url );
888 
889         MavenProject artifactProject;
890         StringBuilder sb = new StringBuilder();
891         try
892         {
893             artifactProject = repoUtils.getMavenProjectFromRepository( artifact );
894             @SuppressWarnings( "unchecked" )
895             List<License> licenses = artifactProject.getLicenses();
896             for ( License license : licenses )
897             {
898                 sb.append( ProjectInfoReportUtils.getArtifactIdCell( license.getName(), license.getUrl() ) );
899             }
900         }
901         catch ( ProjectBuildingException e )
902         {
903             log.warn( "Unable to create Maven project from repository.", e );
904         }
905 
906         String content[];
907         if ( withClassifier )
908         {
909             content =
910                 new String[] { artifact.getGroupId(), artifactIdCell, artifact.getVersion(), artifact.getClassifier(),
911                     artifact.getType(), sb.toString(), isOptional };
912         }
913         else
914         {
915             content =
916                 new String[] { artifact.getGroupId(), artifactIdCell, artifact.getVersion(), artifact.getType(),
917                     sb.toString(), isOptional };
918         }
919 
920         tableRow( withOptional, content );
921     }
922 
923     private void printDependencyListing( DependencyNode node )
924     {
925         Artifact artifact = node.getArtifact();
926         String id = artifact.getId();
927         String dependencyDetailId = "_dep" + idCounter++;
928         String imgId = "_img" + idCounter++;
929 
930         sink.listItem();
931 
932         sink.text( id + ( StringUtils.isNotEmpty( artifact.getScope() ) ? " (" + artifact.getScope() + ") " : " " ) );
933         sink.rawText( "<img id=\"" + imgId + "\" src=\"" + IMG_INFO_URL
934             + "\" alt=\"Information\" onclick=\"toggleDependencyDetail( '" + dependencyDetailId + "', '" + imgId
935             + "' );\" style=\"cursor: pointer;vertical-align:text-bottom;\"></img>" );
936 
937         printDescriptionsAndURLs( node, dependencyDetailId );
938 
939         if ( !node.getChildren().isEmpty() )
940         {
941             boolean toBeIncluded = false;
942             List<DependencyNode> subList = new ArrayList<DependencyNode>();
943             for ( DependencyNode dep : node.getChildren() )
944             {
945                 if ( dependencies.getAllDependencies().contains( dep.getArtifact() ) )
946                 {
947                     subList.add( dep );
948                     toBeIncluded = true;
949                 }
950             }
951 
952             if ( toBeIncluded )
953             {
954                 sink.list();
955                 for ( DependencyNode dep : subList )
956                 {
957                     printDependencyListing( dep );
958                 }
959                 sink.list_();
960             }
961         }
962 
963         sink.listItem_();
964     }
965 
966     private void printDescriptionsAndURLs( DependencyNode node, String uid )
967     {
968         Artifact artifact = node.getArtifact();
969         String id = artifact.getId();
970         String unknownLicenseMessage = getI18nString( "graph.tables.unknown" );
971 
972         sink.rawText( "<div id=\"" + uid + "\" style=\"display:none\">" );
973 
974         sink.table();
975 
976         if ( !Artifact.SCOPE_SYSTEM.equals( artifact.getScope() ) )
977         {
978             try
979             {
980                 MavenProject artifactProject = repoUtils.getMavenProjectFromRepository( artifact );
981                 String artifactDescription = artifactProject.getDescription();
982                 String artifactUrl = artifactProject.getUrl();
983                 String artifactName = artifactProject.getName();
984                 @SuppressWarnings( "unchecked" )
985                 List<License> licenses = artifactProject.getLicenses();
986 
987                 sink.tableRow();
988                 sink.tableHeaderCell();
989                 sink.text( artifactName );
990                 sink.tableHeaderCell_();
991                 sink.tableRow_();
992 
993                 sink.tableRow();
994                 sink.tableCell();
995 
996                 sink.paragraph();
997                 sink.bold();
998                 sink.text( getI18nString( "column.description" ) + ": " );
999                 sink.bold_();
1000                 if ( StringUtils.isNotEmpty( artifactDescription ) )
1001                 {
1002                     sink.text( artifactDescription );
1003                 }
1004                 else
1005                 {
1006                     sink.text( getI18nString( "index", "nodescription" ) );
1007                 }
1008                 sink.paragraph_();
1009 
1010                 if ( StringUtils.isNotEmpty( artifactUrl ) )
1011                 {
1012                     sink.paragraph();
1013                     sink.bold();
1014                     sink.text( getI18nString( "column.url" ) + ": " );
1015                     sink.bold_();
1016                     if ( ProjectInfoReportUtils.isArtifactUrlValid( artifactUrl ) )
1017                     {
1018                         sink.link( artifactUrl );
1019                         sink.text( artifactUrl );
1020                         sink.link_();
1021                     }
1022                     else
1023                     {
1024                         sink.text( artifactUrl );
1025                     }
1026                     sink.paragraph_();
1027                 }
1028 
1029                 sink.paragraph();
1030                 sink.bold();
1031                 sink.text( getI18nString( "license", "title" ) + ": " );
1032                 sink.bold_();
1033                 if ( !licenses.isEmpty() )
1034                 {
1035 
1036                     for ( Iterator<License> it = licenses.iterator(); it.hasNext(); )
1037                     {
1038                         License license = it.next();
1039 
1040                         String licenseName = license.getName();
1041                         if ( StringUtils.isEmpty( licenseName ) )
1042                         {
1043                             licenseName = getI18nString( "unnamed" );
1044                         }
1045 
1046                         String licenseUrl = license.getUrl();
1047 
1048                         if ( licenseUrl != null )
1049                         {
1050                             sink.link( licenseUrl );
1051                         }
1052                         sink.text( licenseName );
1053 
1054                         if ( licenseUrl != null )
1055                         {
1056                             sink.link_();
1057                         }
1058 
1059                         if ( it.hasNext() )
1060                         {
1061                             sink.text( ", " );
1062                         }
1063 
1064                         licenseMap.put( licenseName, artifactName );
1065                     }
1066                 }
1067                 else
1068                 {
1069                     sink.text( getI18nString( "license", "nolicense" ) );
1070 
1071                     licenseMap.put( unknownLicenseMessage, artifactName );
1072                 }
1073                 sink.paragraph_();
1074             }
1075             catch ( ProjectBuildingException e )
1076             {
1077                 log.warn( "Unable to create Maven project from repository for artifact " + artifact.getId(), e );
1078             }
1079         }
1080         else
1081         {
1082             sink.tableRow();
1083             sink.tableHeaderCell();
1084             sink.text( id );
1085             sink.tableHeaderCell_();
1086             sink.tableRow_();
1087 
1088             sink.tableRow();
1089             sink.tableCell();
1090 
1091             sink.paragraph();
1092             sink.bold();
1093             sink.text( getI18nString( "column.description" ) + ": " );
1094             sink.bold_();
1095             sink.text( getI18nString( "index", "nodescription" ) );
1096             sink.paragraph_();
1097 
1098             if ( artifact.getFile() != null )
1099             {
1100                 sink.paragraph();
1101                 sink.bold();
1102                 sink.text( getI18nString( "column.url" ) + ": " );
1103                 sink.bold_();
1104                 sink.text( artifact.getFile().getAbsolutePath() );
1105                 sink.paragraph_();
1106             }
1107         }
1108 
1109         sink.tableCell_();
1110         sink.tableRow_();
1111 
1112         sink.table_();
1113 
1114         sink.rawText( "</div>" );
1115     }
1116 
1117     private void printGroupedLicenses()
1118     {
1119         for ( Map.Entry<String, Object> entry : licenseMap.entrySet() )
1120         {
1121             String licenseName = entry.getKey();
1122             if ( StringUtils.isEmpty( licenseName ) )
1123             {
1124                 licenseName = getI18nString( "unnamed" );
1125             }
1126 
1127             sink.paragraph();
1128             sink.bold();
1129             sink.text( licenseName );
1130             sink.text( ": " );
1131             sink.bold_();
1132 
1133             @SuppressWarnings( "unchecked" )
1134             SortedSet<String> projects = (SortedSet<String>) entry.getValue();
1135 
1136             for ( Iterator<String> iterator = projects.iterator(); iterator.hasNext(); )
1137             {
1138                 String projectName = iterator.next();
1139                 sink.text( projectName );
1140                 if ( iterator.hasNext() )
1141                 {
1142                     sink.text( ", " );
1143                 }
1144             }
1145 
1146             sink.paragraph_();
1147         }
1148     }
1149 
1150     private void printRepositories( Map<String, ArtifactRepository> repoMap, List<String> repoUrlBlackListed )
1151     {
1152         // i18n
1153         String repoid = getI18nString( "repo.locations.column.repoid" );
1154         String url = getI18nString( "repo.locations.column.url" );
1155         String release = getI18nString( "repo.locations.column.release" );
1156         String snapshot = getI18nString( "repo.locations.column.snapshot" );
1157         String blacklisted = getI18nString( "repo.locations.column.blacklisted" );
1158         String releaseEnabled = getI18nString( "repo.locations.cell.release.enabled" );
1159         String releaseDisabled = getI18nString( "repo.locations.cell.release.disabled" );
1160         String snapshotEnabled = getI18nString( "repo.locations.cell.snapshot.enabled" );
1161         String snapshotDisabled = getI18nString( "repo.locations.cell.snapshot.disabled" );
1162         String blacklistedEnabled = getI18nString( "repo.locations.cell.blacklisted.enabled" );
1163         String blacklistedDisabled = getI18nString( "repo.locations.cell.blacklisted.disabled" );
1164 
1165         // Table header
1166 
1167         String[] tableHeader;
1168         int[] justificationRepo;
1169         if ( repoUrlBlackListed.isEmpty() )
1170         {
1171             tableHeader = new String[] { repoid, url, release, snapshot };
1172             justificationRepo =
1173                 new int[] { Sink.JUSTIFY_LEFT, Sink.JUSTIFY_LEFT, Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER };
1174         }
1175         else
1176         {
1177             tableHeader = new String[] { repoid, url, release, snapshot, blacklisted };
1178             justificationRepo =
1179                 new int[] { Sink.JUSTIFY_LEFT, Sink.JUSTIFY_LEFT, Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER,
1180                     Sink.JUSTIFY_CENTER };
1181         }
1182 
1183         startTable( justificationRepo, false );
1184 
1185         tableHeader( tableHeader );
1186 
1187         // Table rows
1188 
1189         for ( ArtifactRepository repo : repoMap.values() )
1190         {
1191             List<ArtifactRepository> mirroredRepos = getMirroredRepositories( repo );
1192 
1193             sink.tableRow();
1194             sink.tableCell();
1195             boolean addLineBreak = false;
1196             for ( ArtifactRepository r : mirroredRepos )
1197             {
1198                 if ( addLineBreak )
1199                 {
1200                     sink.lineBreak();
1201                 }
1202                 addLineBreak = true;
1203                 sink.text( r.getId() );
1204             }
1205             sink.tableCell_();
1206 
1207             sink.tableCell();
1208             addLineBreak = false;
1209             for ( ArtifactRepository r : mirroredRepos )
1210             {
1211                 if ( addLineBreak )
1212                 {
1213                     sink.lineBreak();
1214                 }
1215                 addLineBreak = true;
1216                 if ( repo.isBlacklisted() )
1217                 {
1218                     sink.text( r.getUrl() );
1219                 }
1220                 else
1221                 {
1222                     sink.link( r.getUrl() );
1223                     sink.text( r.getUrl() );
1224                     sink.link_();
1225                 }
1226             }
1227             sink.tableCell_();
1228 
1229             ArtifactRepositoryPolicy releasePolicy = repo.getReleases();
1230             tableCell( releasePolicy.isEnabled() ? releaseEnabled : releaseDisabled );
1231 
1232             ArtifactRepositoryPolicy snapshotPolicy = repo.getSnapshots();
1233             tableCell( snapshotPolicy.isEnabled() ? snapshotEnabled : snapshotDisabled );
1234 
1235             if ( !repoUrlBlackListed.isEmpty() )
1236             {
1237                 tableCell( repoUrlBlackListed.contains( repo.getUrl() ) ? blacklistedEnabled : blacklistedDisabled );
1238             }
1239 
1240             sink.tableRow_();
1241         }
1242 
1243         endTable();
1244     }
1245 
1246     private Object invoke( Object object, String method )
1247         throws IllegalAccessException, InvocationTargetException, NoSuchMethodException
1248     {
1249         return object.getClass().getMethod( method ).invoke( object );
1250     }
1251 
1252     /**
1253      * Get the repos that can be hidden behind a mirror.
1254      *
1255      * @param repo the repo used to download artifacts
1256      * @return the mirrored repositories or a singleton with actual repo if it is not a mirror
1257      */
1258     private List<ArtifactRepository> getMirroredRepositories( ArtifactRepository repo )
1259     {
1260         try
1261         {
1262             @SuppressWarnings( "unchecked" )
1263             List<ArtifactRepository> mirroredRepos =
1264                 (List<ArtifactRepository>) invoke( repo, "getMirroredRepositories" );
1265 
1266             if ( ( mirroredRepos != null ) && ( !mirroredRepos.isEmpty() ) )
1267             {
1268                 return mirroredRepos;
1269             }
1270         }
1271         catch ( IllegalArgumentException e )
1272         {
1273             // ignore: API not available before Maven 3.0.3
1274         }
1275         catch ( SecurityException e )
1276         {
1277             // ignore: API not available before Maven 3.0.3
1278         }
1279         catch ( IllegalAccessException e )
1280         {
1281             // ignore: API not available before Maven 3.0.3
1282         }
1283         catch ( InvocationTargetException e )
1284         {
1285             // ignore: API not available before Maven 3.0.3
1286         }
1287         catch ( NoSuchMethodException e )
1288         {
1289             // ignore: API not available before Maven 3.0.3
1290         }
1291         // before Maven 3.0.3, we can't do anything: Maven 3.0-alpha to 3.0.2 will show the mirror
1292         return Collections.singletonList( repo );
1293     }
1294 
1295     private void printArtifactsLocations( Map<String, ArtifactRepository> repoMap, List<String> repoUrlBlackListed,
1296                                           List<Artifact> alldeps )
1297     {
1298         // i18n
1299         String artifact = getI18nString( "repo.locations.column.artifact" );
1300 
1301         sink.paragraph();
1302         sink.text( getI18nString( "repo.locations.artifact.breakdown" ) );
1303         sink.paragraph_();
1304 
1305         List<String> repoIdList = new ArrayList<String>();
1306         // removed blacklisted repo
1307         for ( Map.Entry<String, ArtifactRepository> entry : repoMap.entrySet() )
1308         {
1309             String repokey = entry.getKey();
1310             ArtifactRepository repo = entry.getValue();
1311             if ( !( repo.isBlacklisted() || repoUrlBlackListed.contains( repo.getUrl() ) ) )
1312             {
1313                 repoIdList.add( repokey );
1314             }
1315         }
1316 
1317         String[] tableHeader = new String[repoIdList.size() + 1];
1318         int[] justificationRepo = new int[repoIdList.size() + 1];
1319 
1320         tableHeader[0] = artifact;
1321         justificationRepo[0] = Sink.JUSTIFY_LEFT;
1322 
1323         int idnum = 1;
1324         for ( String id : repoIdList )
1325         {
1326             tableHeader[idnum] = id;
1327             justificationRepo[idnum] = Sink.JUSTIFY_CENTER;
1328             idnum++;
1329         }
1330 
1331         Map<String, Integer> totalByRepo = new HashMap<String, Integer>();
1332         TotalCell totaldeps = new TotalCell( DEFAULT_DECIMAL_FORMAT );
1333 
1334         startTable( justificationRepo, false );
1335 
1336         tableHeader( tableHeader );
1337 
1338         for ( Artifact dependency : alldeps )
1339         {
1340             totaldeps.incrementTotal( dependency.getScope() );
1341 
1342             sink.tableRow();
1343 
1344             tableCell( dependency.getId() );
1345 
1346             if ( Artifact.SCOPE_SYSTEM.equals( dependency.getScope() ) )
1347             {
1348                 for ( @SuppressWarnings( "unused" )
1349                 String repoId : repoIdList )
1350                 {
1351                     tableCell( "-" );
1352                 }
1353             }
1354             else
1355             {
1356                 for ( String repokey : repoIdList )
1357                 {
1358                     ArtifactRepository repo = repoMap.get( repokey );
1359 
1360                     String depUrl = repoUtils.getDependencyUrlFromRepository( dependency, repo );
1361 
1362                     Integer old = totalByRepo.get( repokey );
1363                     if ( old == null )
1364                     {
1365                         old = 0;
1366                         totalByRepo.put( repokey, old );
1367                     }
1368 
1369                     boolean dependencyExists = false;
1370                     // check snapshots in snapshots repository only and releases in release repositories...
1371                     if ( ( dependency.isSnapshot() && repo.getSnapshots().isEnabled() )
1372                         || ( !dependency.isSnapshot() && repo.getReleases().isEnabled() ) )
1373                     {
1374                         dependencyExists = repoUtils.dependencyExistsInRepo( repo, dependency );
1375                     }
1376 
1377                     if ( dependencyExists )
1378                     {
1379                         sink.tableCell();
1380                         if ( StringUtils.isNotEmpty( depUrl ) )
1381                         {
1382                             sink.link( depUrl );
1383                         }
1384                         else
1385                         {
1386                             sink.text( depUrl );
1387                         }
1388 
1389                         sink.figure();
1390                         sink.figureCaption();
1391                         sink.text( "Found at " + repo.getUrl() );
1392                         sink.figureCaption_();
1393                         sink.figureGraphics( "images/icon_success_sml.gif" );
1394                         sink.figure_();
1395 
1396                         sink.link_();
1397                         sink.tableCell_();
1398 
1399                         totalByRepo.put( repokey, old.intValue() + 1 );
1400                     }
1401                     else
1402                     {
1403                         tableCell( "-" );
1404                     }
1405                 }
1406             }
1407 
1408             sink.tableRow_();
1409         }
1410 
1411         // Total row
1412 
1413         // reused key
1414         tableHeader[0] = getI18nString( "file.details.total" );
1415         tableHeader( tableHeader );
1416         String[] totalRow = new String[repoIdList.size() + 1];
1417         totalRow[0] = totaldeps.toString();
1418         idnum = 1;
1419         for ( String repokey : repoIdList )
1420         {
1421             Integer deps = totalByRepo.get( repokey );
1422             totalRow[idnum++] = deps != null ? deps.toString() : "0";
1423         }
1424 
1425         tableRow( totalRow );
1426 
1427         endTable();
1428     }
1429 
1430     /**
1431      * @param artifacts not null
1432      * @return <code>true</code> if one artifact in the list has a classifier, <code>false</code> otherwise.
1433      */
1434     private boolean hasClassifier( List<Artifact> artifacts )
1435     {
1436         for ( Artifact artifact : artifacts )
1437         {
1438             if ( StringUtils.isNotEmpty( artifact.getClassifier() ) )
1439             {
1440                 return true;
1441             }
1442         }
1443 
1444         return false;
1445     }
1446 
1447     /**
1448      * @param artifacts not null
1449      * @return <code>true</code> if one artifact in the list is optional, <code>false</code> otherwise.
1450      */
1451     private boolean hasOptional( List<Artifact> artifacts )
1452     {
1453         for ( Artifact artifact : artifacts )
1454         {
1455             if ( artifact.isOptional() )
1456             {
1457                 return true;
1458             }
1459         }
1460 
1461         return false;
1462     }
1463 
1464     /**
1465      * @param artifacts not null
1466      * @return <code>true</code> if one artifact in the list is sealed, <code>false</code> otherwise.
1467      */
1468     private boolean hasSealed( List<Artifact> artifacts )
1469     {
1470         for ( Artifact artifact : artifacts )
1471         {
1472             // TODO site:run Why do we need to resolve this...
1473             if ( artifact.getFile() == null )
1474             {
1475                 if ( Artifact.SCOPE_SYSTEM.equals( artifact.getScope() ) )
1476                 {
1477                     // can not resolve system scope artifact file
1478                     continue;
1479                 }
1480 
1481                 try
1482                 {
1483                     repoUtils.resolve( artifact );
1484                 }
1485                 catch ( ArtifactResolutionException e )
1486                 {
1487                     log.error( "Artifact " + artifact.getId() + " can't be resolved.", e );
1488                     continue;
1489                 }
1490                 catch ( ArtifactNotFoundException e )
1491                 {
1492                     if ( ( dependencies.getProject().getGroupId().equals( artifact.getGroupId() ) )
1493                         && ( dependencies.getProject().getArtifactId().equals( artifact.getArtifactId() ) )
1494                         && ( dependencies.getProject().getVersion().equals( artifact.getVersion() ) ) )
1495                     {
1496                         log.warn( "The artifact of this project has never been deployed." );
1497                     }
1498                     else
1499                     {
1500                         log.error( "Artifact " + artifact.getId() + " not found.", e );
1501                     }
1502 
1503                     continue;
1504                 }
1505 
1506                 if ( artifact.getFile() == null )
1507                 {
1508                     log.error( "Artifact " + artifact.getId() + " has no file, even after resolution." );
1509                     continue;
1510                 }
1511             }
1512 
1513             if ( JAR_SUBTYPE.contains( artifact.getType().toLowerCase() ) )
1514             {
1515                 try
1516                 {
1517                     JarData jarDetails = dependencies.getJarDependencyDetails( artifact );
1518                     if ( jarDetails.isSealed() )
1519                     {
1520                         return true;
1521                     }
1522                 }
1523                 catch ( IOException e )
1524                 {
1525                     log.error( "Artifact: " + artifact.getId() + " caused IOException: " + e.getMessage(), e );
1526                 }
1527             }
1528         }
1529         return false;
1530     }
1531 
1532     // CHECKSTYLE_OFF: LineLength
1533     /**
1534      * Formats file length with the associated <a href="https://en.wikipedia.org/wiki/Metric_prefix">SI</a> prefix
1535      * (GB, MB, kB) and using the pattern <code>###0.00</code> by default.
1536      *
1537      * @see <a href="https://en.wikipedia.org/wiki/Metric_prefix">https://en.wikipedia.org/wiki/Metric_prefix</a>
1538      * @see <a href="https://en.wikipedia.org/wiki/Binary_prefix">https://en.wikipedia.org/wiki/Binary_prefix</a>
1539      * @see <a
1540      *      href="https://en.wikipedia.org/wiki/Octet_%28computing%29">https://en.wikipedia.org/wiki/Octet_(computing)</a>
1541      */
1542     // CHECKSTYLE_ON: LineLength
1543     static class FileDecimalFormat
1544         extends DecimalFormat
1545     {
1546         private static final long serialVersionUID = 4062503546523610081L;
1547 
1548         private final I18N i18n;
1549 
1550         private final Locale locale;
1551 
1552         /**
1553          * Default constructor
1554          *
1555          * @param i18n
1556          * @param locale
1557          */
1558         public FileDecimalFormat( I18N i18n, Locale locale )
1559         {
1560             super( "###0.00" );
1561 
1562             this.i18n = i18n;
1563             this.locale = locale;
1564         }
1565 
1566         /** {@inheritDoc} */
1567         public StringBuffer format( long fs, StringBuffer result, FieldPosition fieldPosition )
1568         {
1569             if ( fs > 1000 * 1000 * 1000 )
1570             {
1571                 result = super.format( (float) fs / ( 1000 * 1000 * 1000 ), result, fieldPosition );
1572                 result.append( " " ).append( getString( "report.dependencies.file.details.column.size.gb" ) );
1573                 return result;
1574             }
1575 
1576             if ( fs > 1000 * 1000 )
1577             {
1578                 result = super.format( (float) fs / ( 1000 * 1000 ), result, fieldPosition );
1579                 result.append( " " ).append( getString( "report.dependencies.file.details.column.size.mb" ) );
1580                 return result;
1581             }
1582 
1583             result = super.format( (float) fs / ( 1000 ), result, fieldPosition );
1584             result.append( " " ).append( getString( "report.dependencies.file.details.column.size.kb" ) );
1585             return result;
1586         }
1587 
1588         private String getString( String key )
1589         {
1590             return i18n.getString( "project-info-report", locale, key );
1591         }
1592     }
1593 
1594     /**
1595      * Combine total and total by scope in a cell.
1596      */
1597     static class TotalCell
1598     {
1599         static final int SCOPES_COUNT = 5;
1600 
1601         final DecimalFormat decimalFormat;
1602 
1603         long total = 0;
1604 
1605         long totalCompileScope = 0;
1606 
1607         long totalTestScope = 0;
1608 
1609         long totalRuntimeScope = 0;
1610 
1611         long totalProvidedScope = 0;
1612 
1613         long totalSystemScope = 0;
1614 
1615         TotalCell( DecimalFormat decimalFormat )
1616         {
1617             this.decimalFormat = decimalFormat;
1618         }
1619 
1620         void incrementTotal( String scope )
1621         {
1622             addTotal( 1, scope );
1623         }
1624 
1625         static String getScope( int index )
1626         {
1627             switch ( index )
1628             {
1629                 case 0:
1630                     return Artifact.SCOPE_COMPILE;
1631                 case 1:
1632                     return Artifact.SCOPE_TEST;
1633                 case 2:
1634                     return Artifact.SCOPE_RUNTIME;
1635                 case 3:
1636                     return Artifact.SCOPE_PROVIDED;
1637                 case 4:
1638                     return Artifact.SCOPE_SYSTEM;
1639                 default:
1640                     return null;
1641             }
1642         }
1643 
1644         long getTotal( int index )
1645         {
1646             switch ( index )
1647             {
1648                 case 0:
1649                     return totalCompileScope;
1650                 case 1:
1651                     return totalTestScope;
1652                 case 2:
1653                     return totalRuntimeScope;
1654                 case 3:
1655                     return totalProvidedScope;
1656                 case 4:
1657                     return totalSystemScope;
1658                 default:
1659                     return total;
1660             }
1661         }
1662 
1663         String getTotalString( int index )
1664         {
1665             long totalString = getTotal( index );
1666 
1667             if ( totalString <= 0 )
1668             {
1669                 return "";
1670             }
1671 
1672             StringBuilder sb = new StringBuilder();
1673             if ( index >= 0 )
1674             {
1675                 sb.append( getScope( index ) ).append( ": " );
1676             }
1677             sb.append( decimalFormat.format( getTotal( index ) ) );
1678             return sb.toString();
1679         }
1680 
1681         void addTotal( long add, String scope )
1682         {
1683             total += add;
1684 
1685             if ( Artifact.SCOPE_COMPILE.equals( scope ) )
1686             {
1687                 totalCompileScope += add;
1688             }
1689             else if ( Artifact.SCOPE_TEST.equals( scope ) )
1690             {
1691                 totalTestScope += add;
1692             }
1693             else if ( Artifact.SCOPE_RUNTIME.equals( scope ) )
1694             {
1695                 totalRuntimeScope += add;
1696             }
1697             else if ( Artifact.SCOPE_PROVIDED.equals( scope ) )
1698             {
1699                 totalProvidedScope += add;
1700             }
1701             else if ( Artifact.SCOPE_SYSTEM.equals( scope ) )
1702             {
1703                 totalSystemScope += add;
1704             }
1705         }
1706 
1707         /** {@inheritDoc} */
1708         public String toString()
1709         {
1710             StringBuilder sb = new StringBuilder();
1711             sb.append( decimalFormat.format( total ) );
1712             sb.append( " (" );
1713 
1714             boolean needSeparator = false;
1715             for ( int i = 0; i < SCOPES_COUNT; i++ )
1716             {
1717                 if ( getTotal( i ) > 0 )
1718                 {
1719                     if ( needSeparator )
1720                     {
1721                         sb.append( ", " );
1722                     }
1723                     sb.append( getTotalString( i ) );
1724                     needSeparator = true;
1725                 }
1726             }
1727 
1728             sb.append( ")" );
1729 
1730             return sb.toString();
1731         }
1732     }
1733 }