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 935177 2015-01-05 21:05:55Z michaelo $
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.error( "Artifact: " + artifact.getId() + " has no file." );
559                 continue;
560             }
561 
562             File artifactFile = dependencies.getFile( artifact );
563 
564             totaldeps.incrementTotal( artifact.getScope() );
565             totaldepsize.addTotal( artifactFile.length(), artifact.getScope() );
566 
567             if ( JAR_SUBTYPE.contains( artifact.getType().toLowerCase() ) )
568             {
569                 try
570                 {
571                     JarData jarDetails = dependencies.getJarDependencyDetails( artifact );
572 
573                     String debugInformationCellValue = debugInformationCellNo;
574                     if ( jarDetails.isDebugPresent() )
575                     {
576                         debugInformationCellValue = debugInformationCellYes;
577                         totalDebugInformation.incrementTotal( artifact.getScope() );
578                     }
579 
580                     totalentries.addTotal( jarDetails.getNumEntries(), artifact.getScope() );
581                     totalclasses.addTotal( jarDetails.getNumClasses(), artifact.getScope() );
582                     totalpackages.addTotal( jarDetails.getNumPackages(), artifact.getScope() );
583 
584                     try
585                     {
586                         if ( jarDetails.getJdkRevision() != null )
587                         {
588                             highestJavaVersion = Math.max( highestJavaVersion,
589                                                      Double.parseDouble( jarDetails.getJdkRevision() ) );
590                         }
591                     }
592                     catch ( NumberFormatException e )
593                     {
594                         // ignore
595                     }
596 
597                     String sealedstr = "";
598                     if ( jarDetails.isSealed() )
599                     {
600                         sealedstr = "sealed";
601                         totalsealed.incrementTotal( artifact.getScope() );
602                     }
603 
604                     String name = artifactFile.getName();
605                     String fileLength = fileLengthDecimalFormat.format( artifactFile.length() );
606 
607                     if ( artifactFile.isDirectory() )
608                     {
609                         File parent = artifactFile.getParentFile();
610                         name = parent.getParentFile().getName() + '/' + parent.getName() + '/' + artifactFile.getName();
611                         fileLength = "-";
612                     }
613 
614                     tableRow( hasSealed,
615                               new String[] { name, fileLength,
616                                   DEFAULT_DECIMAL_FORMAT.format( jarDetails.getNumEntries() ),
617                                   DEFAULT_DECIMAL_FORMAT.format( jarDetails.getNumClasses() ),
618                                   DEFAULT_DECIMAL_FORMAT.format( jarDetails.getNumPackages() ),
619                                   jarDetails.getJdkRevision(), debugInformationCellValue, sealedstr } );
620                 }
621                 catch ( IOException e )
622                 {
623                     createExceptionInfoTableRow( artifact, artifactFile, e, hasSealed );
624                 }
625             }
626             else
627             {
628                 tableRow( hasSealed,
629                           new String[] { artifactFile.getName(),
630                               fileLengthDecimalFormat.format( artifactFile.length() ), "", "", "", "", "", "" } );
631             }
632         }
633 
634         // Total raws
635         tableHeader[0] = getI18nString( "file.details.total" );
636         tableHeader( tableHeader );
637 
638         justification[0] = Sink.JUSTIFY_RIGHT;
639         justification[6] = Sink.JUSTIFY_RIGHT;
640 
641         for ( int i = -1; i < TotalCell.SCOPES_COUNT; i++ )
642         {
643             if ( totaldeps.getTotal( i ) > 0 )
644             {
645                 tableRow( hasSealed,
646                           new String[] { totaldeps.getTotalString( i ), totaldepsize.getTotalString( i ),
647                               totalentries.getTotalString( i ), totalclasses.getTotalString( i ),
648                               totalpackages.getTotalString( i ), ( i < 0 ) ? String.valueOf( highestJavaVersion ) : "",
649                               totalDebugInformation.getTotalString( i ), totalsealed.getTotalString( i ) } );
650             }
651         }
652 
653         endTable();
654         endSection();
655     }
656 
657     // Almost as same as in the abstract class but includes the title attribute
658     private void tableHeader( String[] content, String[] titles )
659     {
660         sink.tableRow();
661 
662         if ( content != null )
663         {
664             if ( titles != null && content.length != titles.length )
665             {
666                 // CHECKSTYLE_OFF: LineLength
667                 throw new IllegalArgumentException( "Length of title array must equal the length of the content array" );
668                 // CHECKSTYLE_ON: LineLength
669             }
670 
671             for ( int i = 0; i < content.length; i++ )
672             {
673                 if ( titles != null )
674                 {
675                     tableHeaderCell( content[i], titles[i] );
676                 }
677                 else
678                 {
679                     tableHeaderCell( content[i] );
680                 }
681             }
682         }
683 
684         sink.tableRow_();
685     }
686 
687     private void tableHeaderCell( String text, String title )
688     {
689         if ( title != null )
690         {
691             SinkEventAttributes attributes = new SinkEventAttributeSet( SinkEventAttributes.TITLE, title );
692             sink.tableHeaderCell( attributes );
693         }
694         else
695         {
696             sink.tableHeaderCell();
697         }
698 
699         text( text );
700 
701         sink.tableHeaderCell_();
702     }
703 
704     private void tableRow( boolean fullRow, String[] content )
705     {
706         sink.tableRow();
707 
708         int count = fullRow ? content.length : ( content.length - 1 );
709 
710         for ( int i = 0; i < count; i++ )
711         {
712             tableCell( content[i] );
713         }
714 
715         sink.tableRow_();
716     }
717 
718     private void createExceptionInfoTableRow( Artifact artifact, File artifactFile, Exception e, boolean hasSealed )
719     {
720         tableRow( hasSealed, new String[] { artifact.getId(), artifactFile.getAbsolutePath(), e.getMessage(), "", "",
721             "", "", "" } );
722     }
723 
724     private void populateRepositoryMap( Map<String, ArtifactRepository> repos, List<ArtifactRepository> rowRepos )
725     {
726         for ( ArtifactRepository repo : rowRepos )
727         {
728             repos.put( repo.getId(), repo );
729         }
730     }
731 
732     private void blacklistRepositoryMap( Map<String, ArtifactRepository> repos, List<String> repoUrlBlackListed )
733     {
734         for ( ArtifactRepository repo : repos.values() )
735         {
736             // ping repo
737             if ( repo.isBlacklisted() )
738             {
739                 repoUrlBlackListed.add( repo.getUrl() );
740             }
741             else
742             {
743                 if ( repoUrlBlackListed.contains( repo.getUrl() ) )
744                 {
745                     repo.setBlacklisted( true );
746                 }
747                 else
748                 {
749                     try
750                     {
751                         URL repoUrl = new URL( repo.getUrl() );
752                         if ( ProjectInfoReportUtils.getContent( repoUrl, settings ) == null )
753                         {
754                             log.warn( "The repository url '" + repoUrl + "' has no stream - Repository '"
755                                 + repo.getId() + "' will be blacklisted." );
756                             repo.setBlacklisted( true );
757                             repoUrlBlackListed.add( repo.getUrl() );
758                         }
759                     }
760                     catch ( IOException e )
761                     {
762                         log.warn( "The repository url '" + repo.getUrl() + "' is invalid - Repository '" + repo.getId()
763                             + "' will be blacklisted." );
764                         repo.setBlacklisted( true );
765                         repoUrlBlackListed.add( repo.getUrl() );
766                     }
767                 }
768             }
769         }
770     }
771 
772     @SuppressWarnings( "unchecked" )
773     private void renderSectionDependencyRepositoryLocations()
774     {
775         startSection( getI18nString( "repo.locations.title" ) );
776 
777         // Collect Alphabetical Dependencies
778         List<Artifact> alldeps = dependencies.getAllDependencies();
779         Collections.sort( alldeps, getArtifactComparator() );
780 
781         // Collect Repositories
782         Map<String, ArtifactRepository> repoMap = new HashMap<String, ArtifactRepository>();
783 
784         populateRepositoryMap( repoMap, repoUtils.getRemoteArtifactRepositories() );
785         for ( Artifact artifact : alldeps )
786         {
787             try
788             {
789                 MavenProject artifactProject = repoUtils.getMavenProjectFromRepository( artifact );
790                 populateRepositoryMap( repoMap, artifactProject.getRemoteArtifactRepositories() );
791             }
792             catch ( ProjectBuildingException e )
793             {
794                 log.warn( "Unable to create Maven project from repository for artifact " + artifact.getId(), e );
795             }
796         }
797 
798         List<String> repoUrlBlackListed = new ArrayList<String>();
799         blacklistRepositoryMap( repoMap, repoUrlBlackListed );
800 
801         // Render Repository List
802 
803         printRepositories( repoMap, repoUrlBlackListed );
804 
805         // Render Artifacts locations
806 
807         printArtifactsLocations( repoMap, repoUrlBlackListed, alldeps );
808 
809         endSection();
810     }
811 
812     private void renderSectionDependencyLicenseListing()
813     {
814         startSection( getI18nString( "graph.tables.licenses" ) );
815         printGroupedLicenses();
816         endSection();
817     }
818 
819     private void renderDependenciesForScope( String scope, List<Artifact> artifacts, boolean isTransitive )
820     {
821         if ( artifacts != null )
822         {
823             boolean withClassifier = hasClassifier( artifacts );
824             boolean withOptional = hasOptional( artifacts );
825             String[] tableHeader = getDependencyTableHeader( withClassifier, withOptional );
826 
827             // can't use straight artifact comparison because we want optional last
828             Collections.sort( artifacts, getArtifactComparator() );
829 
830             String anchorByScope =
831                 ( isTransitive ? getI18nString( "transitive.title" ) + "_" + scope : getI18nString( "title" ) + "_"
832                     + scope );
833             startSection( anchorByScope, scope );
834 
835             paragraph( getI18nString( "intro." + scope ) );
836 
837             startTable();
838             tableHeader( tableHeader );
839             for ( Artifact artifact : artifacts )
840             {
841                 renderArtifactRow( artifact, withClassifier, withOptional );
842             }
843             endTable();
844 
845             endSection();
846         }
847     }
848 
849     private Comparator<Artifact> getArtifactComparator()
850     {
851         return new Comparator<Artifact>()
852         {
853             public int compare( Artifact a1, Artifact a2 )
854             {
855                 // put optional last
856                 if ( a1.isOptional() && !a2.isOptional() )
857                 {
858                     return +1;
859                 }
860                 else if ( !a1.isOptional() && a2.isOptional() )
861                 {
862                     return -1;
863                 }
864                 else
865                 {
866                     return a1.compareTo( a2 );
867                 }
868             }
869         };
870     }
871 
872     /**
873      * @param artifact not null
874      * @param withClassifier <code>true</code> to include the classifier column, <code>false</code> otherwise.
875      * @param withOptional <code>true</code> to include the optional column, <code>false</code> otherwise.
876      * @see #getDependencyTableHeader(boolean, boolean)
877      */
878     private void renderArtifactRow( Artifact artifact, boolean withClassifier, boolean withOptional )
879     {
880         String isOptional =
881             artifact.isOptional() ? getI18nString( "column.isOptional" ) : getI18nString( "column.isNotOptional" );
882 
883         String url =
884             ProjectInfoReportUtils.getArtifactUrl( artifactFactory, artifact, mavenProjectBuilder, remoteRepositories,
885                                                    localRepository );
886         String artifactIdCell = ProjectInfoReportUtils.getArtifactIdCell( artifact.getArtifactId(), url );
887 
888         MavenProject artifactProject;
889         StringBuilder sb = new StringBuilder();
890         try
891         {
892             artifactProject = repoUtils.getMavenProjectFromRepository( artifact );
893             @SuppressWarnings( "unchecked" )
894             List<License> licenses = artifactProject.getLicenses();
895             for ( License license : licenses )
896             {
897                 sb.append( ProjectInfoReportUtils.getArtifactIdCell( license.getName(), license.getUrl() ) );
898             }
899         }
900         catch ( ProjectBuildingException e )
901         {
902             log.warn( "Unable to create Maven project from repository.", e );
903         }
904 
905         String content[];
906         if ( withClassifier )
907         {
908             content =
909                 new String[] { artifact.getGroupId(), artifactIdCell, artifact.getVersion(), artifact.getClassifier(),
910                     artifact.getType(), sb.toString(), isOptional };
911         }
912         else
913         {
914             content =
915                 new String[] { artifact.getGroupId(), artifactIdCell, artifact.getVersion(), artifact.getType(),
916                     sb.toString(), isOptional };
917         }
918 
919         tableRow( withOptional, content );
920     }
921 
922     private void printDependencyListing( DependencyNode node )
923     {
924         Artifact artifact = node.getArtifact();
925         String id = artifact.getId();
926         String dependencyDetailId = "_dep" + idCounter++;
927         String imgId = "_img" + idCounter++;
928 
929         sink.listItem();
930 
931         sink.text( id + ( StringUtils.isNotEmpty( artifact.getScope() ) ? " (" + artifact.getScope() + ") " : " " ) );
932         sink.rawText( "<img id=\"" + imgId + "\" src=\"" + IMG_INFO_URL
933             + "\" alt=\"Information\" onclick=\"toggleDependencyDetail( '" + dependencyDetailId + "', '" + imgId
934             + "' );\" style=\"cursor: pointer;vertical-align:text-bottom;\"></img>" );
935 
936         printDescriptionsAndURLs( node, dependencyDetailId );
937 
938         if ( !node.getChildren().isEmpty() )
939         {
940             boolean toBeIncluded = false;
941             List<DependencyNode> subList = new ArrayList<DependencyNode>();
942             for ( DependencyNode dep : node.getChildren() )
943             {
944                 if ( dependencies.getAllDependencies().contains( dep.getArtifact() ) )
945                 {
946                     subList.add( dep );
947                     toBeIncluded = true;
948                 }
949             }
950 
951             if ( toBeIncluded )
952             {
953                 sink.list();
954                 for ( DependencyNode dep : subList )
955                 {
956                     printDependencyListing( dep );
957                 }
958                 sink.list_();
959             }
960         }
961 
962         sink.listItem_();
963     }
964 
965     private void printDescriptionsAndURLs( DependencyNode node, String uid )
966     {
967         Artifact artifact = node.getArtifact();
968         String id = artifact.getId();
969         String unknownLicenseMessage = getI18nString( "graph.tables.unknown" );
970 
971         sink.rawText( "<div id=\"" + uid + "\" style=\"display:none\">" );
972 
973         sink.table();
974 
975         if ( !Artifact.SCOPE_SYSTEM.equals( artifact.getScope() ) )
976         {
977             try
978             {
979                 MavenProject artifactProject = repoUtils.getMavenProjectFromRepository( artifact );
980                 String artifactDescription = artifactProject.getDescription();
981                 String artifactUrl = artifactProject.getUrl();
982                 String artifactName = artifactProject.getName();
983                 @SuppressWarnings( "unchecked" )
984                 List<License> licenses = artifactProject.getLicenses();
985 
986                 sink.tableRow();
987                 sink.tableHeaderCell();
988                 sink.text( artifactName );
989                 sink.tableHeaderCell_();
990                 sink.tableRow_();
991 
992                 sink.tableRow();
993                 sink.tableCell();
994 
995                 sink.paragraph();
996                 sink.bold();
997                 sink.text( getI18nString( "column.description" ) + ": " );
998                 sink.bold_();
999                 if ( StringUtils.isNotEmpty( artifactDescription ) )
1000                 {
1001                     sink.text( artifactDescription );
1002                 }
1003                 else
1004                 {
1005                     sink.text( getI18nString( "index", "nodescription" ) );
1006                 }
1007                 sink.paragraph_();
1008 
1009                 if ( StringUtils.isNotEmpty( artifactUrl ) )
1010                 {
1011                     sink.paragraph();
1012                     sink.bold();
1013                     sink.text( getI18nString( "column.url" ) + ": " );
1014                     sink.bold_();
1015                     if ( ProjectInfoReportUtils.isArtifactUrlValid( artifactUrl ) )
1016                     {
1017                         sink.link( artifactUrl );
1018                         sink.text( artifactUrl );
1019                         sink.link_();
1020                     }
1021                     else
1022                     {
1023                         sink.text( artifactUrl );
1024                     }
1025                     sink.paragraph_();
1026                 }
1027 
1028                 sink.paragraph();
1029                 sink.bold();
1030                 sink.text( getI18nString( "license", "title" ) + ": " );
1031                 sink.bold_();
1032                 if ( !licenses.isEmpty() )
1033                 {
1034 
1035                     for ( Iterator<License> it = licenses.iterator(); it.hasNext(); )
1036                     {
1037                         License license = it.next();
1038 
1039                         String licenseName = license.getName();
1040                         if ( StringUtils.isEmpty( licenseName ) )
1041                         {
1042                             licenseName = getI18nString( "unnamed" );
1043                         }
1044 
1045                         String licenseUrl = license.getUrl();
1046 
1047                         if ( licenseUrl != null )
1048                         {
1049                             sink.link( licenseUrl );
1050                         }
1051                         sink.text( licenseName );
1052 
1053                         if ( licenseUrl != null )
1054                         {
1055                             sink.link_();
1056                         }
1057 
1058                         if ( it.hasNext() )
1059                         {
1060                             sink.text( ", " );
1061                         }
1062 
1063                         licenseMap.put( licenseName, artifactName );
1064                     }
1065                 }
1066                 else
1067                 {
1068                     sink.text( getI18nString( "license", "nolicense" ) );
1069 
1070                     licenseMap.put( unknownLicenseMessage, artifactName );
1071                 }
1072                 sink.paragraph_();
1073             }
1074             catch ( ProjectBuildingException e )
1075             {
1076                 log.warn( "Unable to create Maven project from repository for artifact " + artifact.getId(), e );
1077             }
1078         }
1079         else
1080         {
1081             sink.tableRow();
1082             sink.tableHeaderCell();
1083             sink.text( id );
1084             sink.tableHeaderCell_();
1085             sink.tableRow_();
1086 
1087             sink.tableRow();
1088             sink.tableCell();
1089 
1090             sink.paragraph();
1091             sink.bold();
1092             sink.text( getI18nString( "column.description" ) + ": " );
1093             sink.bold_();
1094             sink.text( getI18nString( "index", "nodescription" ) );
1095             sink.paragraph_();
1096 
1097             if ( artifact.getFile() != null )
1098             {
1099                 sink.paragraph();
1100                 sink.bold();
1101                 sink.text( getI18nString( "column.url" ) + ": " );
1102                 sink.bold_();
1103                 sink.text( artifact.getFile().getAbsolutePath() );
1104                 sink.paragraph_();
1105             }
1106         }
1107 
1108         sink.tableCell_();
1109         sink.tableRow_();
1110 
1111         sink.table_();
1112 
1113         sink.rawText( "</div>" );
1114     }
1115 
1116     private void printGroupedLicenses()
1117     {
1118         for ( Map.Entry<String, Object> entry : licenseMap.entrySet() )
1119         {
1120             String licenseName = entry.getKey();
1121             if ( StringUtils.isEmpty( licenseName ) )
1122             {
1123                 licenseName = getI18nString( "unnamed" );
1124             }
1125 
1126             sink.paragraph();
1127             sink.bold();
1128             sink.text( licenseName );
1129             sink.text( ": " );
1130             sink.bold_();
1131 
1132             @SuppressWarnings( "unchecked" )
1133             SortedSet<String> projects = (SortedSet<String>) entry.getValue();
1134 
1135             for ( Iterator<String> iterator = projects.iterator(); iterator.hasNext(); )
1136             {
1137                 String projectName = iterator.next();
1138                 sink.text( projectName );
1139                 if ( iterator.hasNext() )
1140                 {
1141                     sink.text( ", " );
1142                 }
1143             }
1144 
1145             sink.paragraph_();
1146         }
1147     }
1148 
1149     private void printRepositories( Map<String, ArtifactRepository> repoMap, List<String> repoUrlBlackListed )
1150     {
1151         // i18n
1152         String repoid = getI18nString( "repo.locations.column.repoid" );
1153         String url = getI18nString( "repo.locations.column.url" );
1154         String release = getI18nString( "repo.locations.column.release" );
1155         String snapshot = getI18nString( "repo.locations.column.snapshot" );
1156         String blacklisted = getI18nString( "repo.locations.column.blacklisted" );
1157         String releaseEnabled = getI18nString( "repo.locations.cell.release.enabled" );
1158         String releaseDisabled = getI18nString( "repo.locations.cell.release.disabled" );
1159         String snapshotEnabled = getI18nString( "repo.locations.cell.snapshot.enabled" );
1160         String snapshotDisabled = getI18nString( "repo.locations.cell.snapshot.disabled" );
1161         String blacklistedEnabled = getI18nString( "repo.locations.cell.blacklisted.enabled" );
1162         String blacklistedDisabled = getI18nString( "repo.locations.cell.blacklisted.disabled" );
1163 
1164         // Table header
1165 
1166         String[] tableHeader;
1167         int[] justificationRepo;
1168         if ( repoUrlBlackListed.isEmpty() )
1169         {
1170             tableHeader = new String[] { repoid, url, release, snapshot };
1171             justificationRepo =
1172                 new int[] { Sink.JUSTIFY_LEFT, Sink.JUSTIFY_LEFT, Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER };
1173         }
1174         else
1175         {
1176             tableHeader = new String[] { repoid, url, release, snapshot, blacklisted };
1177             justificationRepo =
1178                 new int[] { Sink.JUSTIFY_LEFT, Sink.JUSTIFY_LEFT, Sink.JUSTIFY_CENTER, Sink.JUSTIFY_CENTER,
1179                     Sink.JUSTIFY_CENTER };
1180         }
1181 
1182         startTable( justificationRepo, false );
1183 
1184         tableHeader( tableHeader );
1185 
1186         // Table rows
1187 
1188         for ( ArtifactRepository repo : repoMap.values() )
1189         {
1190             List<ArtifactRepository> mirroredRepos = getMirroredRepositories( repo );
1191 
1192             sink.tableRow();
1193             sink.tableCell();
1194             boolean addLineBreak = false;
1195             for ( ArtifactRepository r : mirroredRepos )
1196             {
1197                 if ( addLineBreak )
1198                 {
1199                     sink.lineBreak();
1200                 }
1201                 addLineBreak = true;
1202                 sink.text( r.getId() );
1203             }
1204             sink.tableCell_();
1205 
1206             sink.tableCell();
1207             addLineBreak = false;
1208             for ( ArtifactRepository r : mirroredRepos )
1209             {
1210                 if ( addLineBreak )
1211                 {
1212                     sink.lineBreak();
1213                 }
1214                 addLineBreak = true;
1215                 if ( repo.isBlacklisted() )
1216                 {
1217                     sink.text( r.getUrl() );
1218                 }
1219                 else
1220                 {
1221                     sink.link( r.getUrl() );
1222                     sink.text( r.getUrl() );
1223                     sink.link_();
1224                 }
1225             }
1226             sink.tableCell_();
1227 
1228             ArtifactRepositoryPolicy releasePolicy = repo.getReleases();
1229             tableCell( releasePolicy.isEnabled() ? releaseEnabled : releaseDisabled );
1230 
1231             ArtifactRepositoryPolicy snapshotPolicy = repo.getSnapshots();
1232             tableCell( snapshotPolicy.isEnabled() ? snapshotEnabled : snapshotDisabled );
1233 
1234             if ( !repoUrlBlackListed.isEmpty() )
1235             {
1236                 tableCell( repoUrlBlackListed.contains( repo.getUrl() ) ? blacklistedEnabled : blacklistedDisabled );
1237             }
1238 
1239             sink.tableRow_();
1240         }
1241 
1242         endTable();
1243     }
1244 
1245     private Object invoke( Object object, String method )
1246         throws IllegalAccessException, InvocationTargetException, NoSuchMethodException
1247     {
1248         return object.getClass().getMethod( method ).invoke( object );
1249     }
1250 
1251     /**
1252      * Get the repos that can be hidden behind a mirror.
1253      *
1254      * @param repo the repo used to download artifacts
1255      * @return the mirrored repositories or a singleton with actual repo if it is not a mirror
1256      */
1257     private List<ArtifactRepository> getMirroredRepositories( ArtifactRepository repo )
1258     {
1259         try
1260         {
1261             @SuppressWarnings( "unchecked" )
1262             List<ArtifactRepository> mirroredRepos =
1263                 (List<ArtifactRepository>) invoke( repo, "getMirroredRepositories" );
1264 
1265             if ( ( mirroredRepos != null ) && ( !mirroredRepos.isEmpty() ) )
1266             {
1267                 return mirroredRepos;
1268             }
1269         }
1270         catch ( IllegalArgumentException e )
1271         {
1272             // ignore: API not available before Maven 3.0.3
1273         }
1274         catch ( SecurityException e )
1275         {
1276             // ignore: API not available before Maven 3.0.3
1277         }
1278         catch ( IllegalAccessException e )
1279         {
1280             // ignore: API not available before Maven 3.0.3
1281         }
1282         catch ( InvocationTargetException e )
1283         {
1284             // ignore: API not available before Maven 3.0.3
1285         }
1286         catch ( NoSuchMethodException e )
1287         {
1288             // ignore: API not available before Maven 3.0.3
1289         }
1290         // before Maven 3.0.3, we can't do anything: Maven 3.0-alpha to 3.0.2 will show the mirror
1291         return Collections.singletonList( repo );
1292     }
1293 
1294     private void printArtifactsLocations( Map<String, ArtifactRepository> repoMap, List<String> repoUrlBlackListed,
1295                                           List<Artifact> alldeps )
1296     {
1297         // i18n
1298         String artifact = getI18nString( "repo.locations.column.artifact" );
1299 
1300         sink.paragraph();
1301         sink.text( getI18nString( "repo.locations.artifact.breakdown" ) );
1302         sink.paragraph_();
1303 
1304         List<String> repoIdList = new ArrayList<String>();
1305         // removed blacklisted repo
1306         for ( Map.Entry<String, ArtifactRepository> entry : repoMap.entrySet() )
1307         {
1308             String repokey = entry.getKey();
1309             ArtifactRepository repo = entry.getValue();
1310             if ( !( repo.isBlacklisted() || repoUrlBlackListed.contains( repo.getUrl() ) ) )
1311             {
1312                 repoIdList.add( repokey );
1313             }
1314         }
1315 
1316         String[] tableHeader = new String[repoIdList.size() + 1];
1317         int[] justificationRepo = new int[repoIdList.size() + 1];
1318 
1319         tableHeader[0] = artifact;
1320         justificationRepo[0] = Sink.JUSTIFY_LEFT;
1321 
1322         int idnum = 1;
1323         for ( String id : repoIdList )
1324         {
1325             tableHeader[idnum] = id;
1326             justificationRepo[idnum] = Sink.JUSTIFY_CENTER;
1327             idnum++;
1328         }
1329 
1330         Map<String, Integer> totalByRepo = new HashMap<String, Integer>();
1331         TotalCell totaldeps = new TotalCell( DEFAULT_DECIMAL_FORMAT );
1332 
1333         startTable( justificationRepo, false );
1334 
1335         tableHeader( tableHeader );
1336 
1337         for ( Artifact dependency : alldeps )
1338         {
1339             totaldeps.incrementTotal( dependency.getScope() );
1340 
1341             sink.tableRow();
1342 
1343             tableCell( dependency.getId() );
1344 
1345             if ( Artifact.SCOPE_SYSTEM.equals( dependency.getScope() ) )
1346             {
1347                 for ( @SuppressWarnings( "unused" )
1348                 String repoId : repoIdList )
1349                 {
1350                     tableCell( "-" );
1351                 }
1352             }
1353             else
1354             {
1355                 for ( String repokey : repoIdList )
1356                 {
1357                     ArtifactRepository repo = repoMap.get( repokey );
1358 
1359                     String depUrl = repoUtils.getDependencyUrlFromRepository( dependency, repo );
1360 
1361                     Integer old = totalByRepo.get( repokey );
1362                     if ( old == null )
1363                     {
1364                         old = 0;
1365                         totalByRepo.put( repokey, old );
1366                     }
1367 
1368                     boolean dependencyExists = false;
1369                     // check snapshots in snapshots repository only and releases in release repositories...
1370                     if ( ( dependency.isSnapshot() && repo.getSnapshots().isEnabled() )
1371                         || ( !dependency.isSnapshot() && repo.getReleases().isEnabled() ) )
1372                     {
1373                         dependencyExists = repoUtils.dependencyExistsInRepo( repo, dependency );
1374                     }
1375 
1376                     if ( dependencyExists )
1377                     {
1378                         sink.tableCell();
1379                         if ( StringUtils.isNotEmpty( depUrl ) )
1380                         {
1381                             sink.link( depUrl );
1382                         }
1383                         else
1384                         {
1385                             sink.text( depUrl );
1386                         }
1387 
1388                         sink.figure();
1389                         sink.figureCaption();
1390                         sink.text( "Found at " + repo.getUrl() );
1391                         sink.figureCaption_();
1392                         sink.figureGraphics( "images/icon_success_sml.gif" );
1393                         sink.figure_();
1394 
1395                         sink.link_();
1396                         sink.tableCell_();
1397 
1398                         totalByRepo.put( repokey, old.intValue() + 1 );
1399                     }
1400                     else
1401                     {
1402                         tableCell( "-" );
1403                     }
1404                 }
1405             }
1406 
1407             sink.tableRow_();
1408         }
1409 
1410         // Total row
1411 
1412         // reused key
1413         tableHeader[0] = getI18nString( "file.details.total" );
1414         tableHeader( tableHeader );
1415         String[] totalRow = new String[repoIdList.size() + 1];
1416         totalRow[0] = totaldeps.toString();
1417         idnum = 1;
1418         for ( String repokey : repoIdList )
1419         {
1420             Integer deps = totalByRepo.get( repokey );
1421             totalRow[idnum++] = deps != null ? deps.toString() : "0";
1422         }
1423 
1424         tableRow( totalRow );
1425 
1426         endTable();
1427     }
1428 
1429     /**
1430      * @param artifacts not null
1431      * @return <code>true</code> if one artifact in the list has a classifier, <code>false</code> otherwise.
1432      */
1433     private boolean hasClassifier( List<Artifact> artifacts )
1434     {
1435         for ( Artifact artifact : artifacts )
1436         {
1437             if ( StringUtils.isNotEmpty( artifact.getClassifier() ) )
1438             {
1439                 return true;
1440             }
1441         }
1442 
1443         return false;
1444     }
1445 
1446     /**
1447      * @param artifacts not null
1448      * @return <code>true</code> if one artifact in the list is optional, <code>false</code> otherwise.
1449      */
1450     private boolean hasOptional( List<Artifact> artifacts )
1451     {
1452         for ( Artifact artifact : artifacts )
1453         {
1454             if ( artifact.isOptional() )
1455             {
1456                 return true;
1457             }
1458         }
1459 
1460         return false;
1461     }
1462 
1463     /**
1464      * @param artifacts not null
1465      * @return <code>true</code> if one artifact in the list is sealed, <code>false</code> otherwise.
1466      */
1467     private boolean hasSealed( List<Artifact> artifacts )
1468     {
1469         for ( Artifact artifact : artifacts )
1470         {
1471             // TODO site:run Why do we need to resolve this...
1472             if ( artifact.getFile() == null )
1473             {
1474                 if ( Artifact.SCOPE_SYSTEM.equals( artifact.getScope() ) )
1475                 {
1476                     // can not resolve system scope artifact file
1477                     continue;
1478                 }
1479 
1480                 try
1481                 {
1482                     repoUtils.resolve( artifact );
1483                 }
1484                 catch ( ArtifactResolutionException e )
1485                 {
1486                     log.error( "Artifact: " + artifact.getId() + " has no file.", e );
1487                     continue;
1488                 }
1489                 catch ( ArtifactNotFoundException e )
1490                 {
1491                     if ( ( dependencies.getProject().getGroupId().equals( artifact.getGroupId() ) )
1492                         && ( dependencies.getProject().getArtifactId().equals( artifact.getArtifactId() ) )
1493                         && ( dependencies.getProject().getVersion().equals( artifact.getVersion() ) ) )
1494                     {
1495                         log.warn( "The artifact of this project has never been deployed." );
1496                     }
1497                     else
1498                     {
1499                         log.error( "Artifact: " + artifact.getId() + " has no file.", e );
1500                     }
1501 
1502                     continue;
1503                 }
1504 
1505                 if ( artifact.getFile() == null )
1506                 {
1507                     log.error( "Artifact: " + artifact.getId() + " has no file, even after resolution." );
1508                     continue;
1509                 }
1510             }
1511 
1512             if ( JAR_SUBTYPE.contains( artifact.getType().toLowerCase() ) )
1513             {
1514                 try
1515                 {
1516                     JarData jarDetails = dependencies.getJarDependencyDetails( artifact );
1517                     if ( jarDetails.isSealed() )
1518                     {
1519                         return true;
1520                     }
1521                 }
1522                 catch ( IOException e )
1523                 {
1524                     log.error( "Artifact: " + artifact.getId() + " caused IOException: " + e.getMessage(), e );
1525                 }
1526             }
1527         }
1528         return false;
1529     }
1530 
1531     // CHECKSTYLE_OFF: LineLength
1532     /**
1533      * Formats file length with the associated <a href="https://en.wikipedia.org/wiki/Metric_prefix">SI</a> prefix
1534      * (GB, MB, kB) and using the pattern <code>###0.00</code> by default.
1535      *
1536      * @see <a href="https://en.wikipedia.org/wiki/Metric_prefix">https://en.wikipedia.org/wiki/Metric_prefix</a>
1537      * @see <a href="https://en.wikipedia.org/wiki/Binary_prefix">https://en.wikipedia.org/wiki/Binary_prefix</a>
1538      * @see <a
1539      *      href="https://en.wikipedia.org/wiki/Octet_%28computing%29">https://en.wikipedia.org/wiki/Octet_(computing)</a>
1540      */
1541     // CHECKSTYLE_ON: LineLength
1542     static class FileDecimalFormat
1543         extends DecimalFormat
1544     {
1545         private static final long serialVersionUID = 4062503546523610081L;
1546 
1547         private final I18N i18n;
1548 
1549         private final Locale locale;
1550 
1551         /**
1552          * Default constructor
1553          *
1554          * @param i18n
1555          * @param locale
1556          */
1557         public FileDecimalFormat( I18N i18n, Locale locale )
1558         {
1559             super( "###0.00" );
1560 
1561             this.i18n = i18n;
1562             this.locale = locale;
1563         }
1564 
1565         /** {@inheritDoc} */
1566         public StringBuffer format( long fs, StringBuffer result, FieldPosition fieldPosition )
1567         {
1568             if ( fs > 1000 * 1000 * 1000 )
1569             {
1570                 result = super.format( (float) fs / ( 1000 * 1000 * 1000 ), result, fieldPosition );
1571                 result.append( " " ).append( getString( "report.dependencies.file.details.column.size.gb" ) );
1572                 return result;
1573             }
1574 
1575             if ( fs > 1000 * 1000 )
1576             {
1577                 result = super.format( (float) fs / ( 1000 * 1000 ), result, fieldPosition );
1578                 result.append( " " ).append( getString( "report.dependencies.file.details.column.size.mb" ) );
1579                 return result;
1580             }
1581 
1582             result = super.format( (float) fs / ( 1000 ), result, fieldPosition );
1583             result.append( " " ).append( getString( "report.dependencies.file.details.column.size.kb" ) );
1584             return result;
1585         }
1586 
1587         private String getString( String key )
1588         {
1589             return i18n.getString( "project-info-report", locale, key );
1590         }
1591     }
1592 
1593     /**
1594      * Combine total and total by scope in a cell.
1595      */
1596     static class TotalCell
1597     {
1598         static final int SCOPES_COUNT = 5;
1599 
1600         final DecimalFormat decimalFormat;
1601 
1602         long total = 0;
1603 
1604         long totalCompileScope = 0;
1605 
1606         long totalTestScope = 0;
1607 
1608         long totalRuntimeScope = 0;
1609 
1610         long totalProvidedScope = 0;
1611 
1612         long totalSystemScope = 0;
1613 
1614         TotalCell( DecimalFormat decimalFormat )
1615         {
1616             this.decimalFormat = decimalFormat;
1617         }
1618 
1619         void incrementTotal( String scope )
1620         {
1621             addTotal( 1, scope );
1622         }
1623 
1624         static String getScope( int index )
1625         {
1626             switch ( index )
1627             {
1628                 case 0:
1629                     return Artifact.SCOPE_COMPILE;
1630                 case 1:
1631                     return Artifact.SCOPE_TEST;
1632                 case 2:
1633                     return Artifact.SCOPE_RUNTIME;
1634                 case 3:
1635                     return Artifact.SCOPE_PROVIDED;
1636                 case 4:
1637                     return Artifact.SCOPE_SYSTEM;
1638                 default:
1639                     return null;
1640             }
1641         }
1642 
1643         long getTotal( int index )
1644         {
1645             switch ( index )
1646             {
1647                 case 0:
1648                     return totalCompileScope;
1649                 case 1:
1650                     return totalTestScope;
1651                 case 2:
1652                     return totalRuntimeScope;
1653                 case 3:
1654                     return totalProvidedScope;
1655                 case 4:
1656                     return totalSystemScope;
1657                 default:
1658                     return total;
1659             }
1660         }
1661 
1662         String getTotalString( int index )
1663         {
1664             long totalString = getTotal( index );
1665 
1666             if ( totalString <= 0 )
1667             {
1668                 return "";
1669             }
1670 
1671             StringBuilder sb = new StringBuilder();
1672             if ( index >= 0 )
1673             {
1674                 sb.append( getScope( index ) ).append( ": " );
1675             }
1676             sb.append( decimalFormat.format( getTotal( index ) ) );
1677             return sb.toString();
1678         }
1679 
1680         void addTotal( long add, String scope )
1681         {
1682             total += add;
1683 
1684             if ( Artifact.SCOPE_COMPILE.equals( scope ) )
1685             {
1686                 totalCompileScope += add;
1687             }
1688             else if ( Artifact.SCOPE_TEST.equals( scope ) )
1689             {
1690                 totalTestScope += add;
1691             }
1692             else if ( Artifact.SCOPE_RUNTIME.equals( scope ) )
1693             {
1694                 totalRuntimeScope += add;
1695             }
1696             else if ( Artifact.SCOPE_PROVIDED.equals( scope ) )
1697             {
1698                 totalProvidedScope += add;
1699             }
1700             else if ( Artifact.SCOPE_SYSTEM.equals( scope ) )
1701             {
1702                 totalSystemScope += add;
1703             }
1704         }
1705 
1706         /** {@inheritDoc} */
1707         public String toString()
1708         {
1709             StringBuilder sb = new StringBuilder();
1710             sb.append( decimalFormat.format( total ) );
1711             sb.append( " (" );
1712 
1713             boolean needSeparator = false;
1714             for ( int i = 0; i < SCOPES_COUNT; i++ )
1715             {
1716                 if ( getTotal( i ) > 0 )
1717                 {
1718                     if ( needSeparator )
1719                     {
1720                         sb.append( ", " );
1721                     }
1722                     sb.append( getTotalString( i ) );
1723                     needSeparator = true;
1724                 }
1725             }
1726 
1727             sb.append( ")" );
1728 
1729             return sb.toString();
1730         }
1731     }
1732 }