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