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