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