1 package org.codehaus.mojo.l10n;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.BufferedInputStream;
23 import java.io.File;
24 import java.io.FileInputStream;
25 import java.io.IOException;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.Comparator;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.Iterator;
32 import java.util.List;
33 import java.util.Locale;
34 import java.util.Map;
35 import java.util.Properties;
36 import java.util.ResourceBundle;
37 import java.util.Set;
38 import java.util.TreeSet;
39 import java.util.regex.Pattern;
40
41 import org.apache.maven.doxia.sink.Sink;
42 import org.apache.maven.model.Resource;
43 import org.apache.maven.plugins.annotations.Mojo;
44 import org.apache.maven.plugins.annotations.Parameter;
45 import org.apache.maven.project.MavenProject;
46 import org.apache.maven.reporting.AbstractMavenReport;
47 import org.apache.maven.reporting.AbstractMavenReportRenderer;
48 import org.apache.maven.reporting.MavenReportException;
49 import org.codehaus.plexus.util.DirectoryScanner;
50 import org.codehaus.plexus.util.IOUtil;
51 import org.codehaus.plexus.util.StringUtils;
52
53
54
55
56
57
58
59
60
61 @Mojo( name = "report" )
62 public class L10NStatusReport
63 extends AbstractMavenReport
64 {
65
66
67
68
69
70 @Parameter( defaultValue = "${reactorProjects}", readonly = true )
71 protected List<MavenProject> reactorProjects;
72
73
74
75
76
77
78 @Parameter
79 private List<String> locales;
80
81
82
83
84
85
86 @Parameter
87 private List<String> excludes;
88
89
90
91
92
93
94 @Parameter
95 private List<String> includes;
96
97
98
99
100
101
102 @Parameter( defaultValue = "false", property = "maven.l10n.aggregate" )
103 protected boolean aggregate;
104
105
106 private static final String[] DEFAULT_INCLUDES = {"**/*.properties"};
107
108 private static final String[] EMPTY_STRING_ARRAY = {};
109
110 public boolean canGenerateReport()
111 {
112 if ( aggregate && !project.isExecutionRoot() )
113 {
114 return false;
115 }
116
117 return !constructResourceDirs().isEmpty();
118 }
119
120
121
122
123
124
125 protected Map constructResourceDirs()
126 {
127 Map sourceDirs = new HashMap();
128 if ( aggregate )
129 {
130 for ( Iterator i = reactorProjects.iterator(); i.hasNext(); )
131 {
132 MavenProject prj = (MavenProject) i.next();
133 if ( prj.getResources() != null && !prj.getResources().isEmpty() )
134 {
135 sourceDirs.put( prj, prj.getResources() );
136 }
137
138 }
139 }
140 else
141 {
142 if ( project.getResources() != null && !project.getResources().isEmpty() )
143 {
144 sourceDirs.put( project, project.getResources() );
145 }
146 }
147 return sourceDirs;
148 }
149
150
151
152
153
154 protected void executeReport( Locale locale )
155 throws MavenReportException
156 {
157 Set included = new TreeSet( new WrapperComparator() );
158 Map res = constructResourceDirs();
159 for ( Iterator it = res.keySet().iterator(); it.hasNext(); )
160 {
161 MavenProject prj = (MavenProject) it.next();
162 List lst = (List) res.get( prj );
163 for ( Iterator i = lst.iterator(); i.hasNext(); )
164 {
165 Resource resource = (Resource) i.next();
166
167 File resourceDirectory = new File( resource.getDirectory() );
168
169 if ( !resourceDirectory.exists() )
170 {
171 getLog().info( "Resource directory does not exist: " + resourceDirectory );
172 continue;
173 }
174
175 DirectoryScanner scanner = new DirectoryScanner();
176
177 scanner.setBasedir( resource.getDirectory() );
178 List allIncludes = new ArrayList();
179 if ( resource.getIncludes() != null && !resource.getIncludes().isEmpty() )
180 {
181 allIncludes.addAll( resource.getIncludes() );
182 }
183 if ( includes != null && !includes.isEmpty() )
184 {
185 allIncludes.addAll( includes );
186 }
187
188 if ( allIncludes.isEmpty() )
189 {
190 scanner.setIncludes( DEFAULT_INCLUDES );
191 }
192 else
193 {
194 scanner.setIncludes( (String[]) allIncludes.toArray( EMPTY_STRING_ARRAY ) );
195 }
196
197 List allExcludes = new ArrayList();
198 if ( resource.getExcludes() != null && !resource.getExcludes().isEmpty() )
199 {
200 allExcludes.addAll( resource.getExcludes() );
201 }
202 else if ( excludes != null && !excludes.isEmpty() )
203 {
204 allExcludes.addAll( excludes );
205 }
206
207 scanner.setExcludes( (String[]) allExcludes.toArray( EMPTY_STRING_ARRAY ) );
208
209 scanner.addDefaultExcludes();
210 scanner.scan();
211
212 List includedFiles = Arrays.asList( scanner.getIncludedFiles() );
213 for ( Iterator j = includedFiles.iterator(); j.hasNext(); )
214 {
215 String name = (String) j.next();
216 File source = new File( resource.getDirectory(), name );
217 included.add( new Wrapper( name, source, prj ) );
218 }
219 }
220 }
221
222
223 L10NStatusRenderer r = new L10NStatusRenderer( getSink(), getBundle( locale ), included, locale );
224 r.render();
225 }
226
227
228
229
230 public String getDescription( Locale locale )
231 {
232 return getBundle( locale ).getString( "report.l10n.description" );
233 }
234
235
236
237
238 public String getName( Locale locale )
239 {
240 return getBundle( locale ).getString( "report.l10n.name" );
241 }
242
243
244
245
246 public String getOutputName()
247 {
248 return "l10n-status";
249 }
250
251 private static ResourceBundle getBundle( Locale locale )
252 {
253 return ResourceBundle.getBundle( "l10n-status-report", locale, L10NStatusReport.class.getClassLoader() );
254 }
255
256
257
258
259
260 class L10NStatusRenderer
261 extends AbstractMavenReportRenderer
262 {
263
264 private final ResourceBundle bundle;
265
266
267
268
269 private final Locale rendererLocale;
270
271 private Set files;
272
273 private Pattern localedPattern = Pattern.compile( ".*_[a-zA-Z]{2}[_]?[a-zA-Z]{0,2}?\\.properties" );
274
275 public L10NStatusRenderer( Sink sink, ResourceBundle bundle, Set files, Locale rendererLocale )
276 {
277 super( sink );
278
279 this.bundle = bundle;
280 this.files = files;
281 this.rendererLocale = rendererLocale;
282 }
283
284
285
286
287 public String getTitle()
288 {
289 return bundle.getString( "report.l10n.title" );
290 }
291
292
293
294
295 public void renderBody()
296 {
297 startSection( getTitle() );
298
299 paragraph( bundle.getString( "report.l10n.intro" ) );
300 startSection( bundle.getString( "report.l10n.summary" ) );
301
302 startTable();
303 tableCaption( bundle.getString( "report.l10n.summary.caption" ) );
304 String defaultLocaleColumnName = bundle.getString( "report.l10n.column.default" );
305 String pathColumnName = bundle.getString( "report.l10n.column.path" );
306 String missingFileLabel = bundle.getString( "report.l10n.missingFile" );
307 String missingKeysLabel = bundle.getString( "report.l10n.missingKey" );
308 String okLabel = bundle.getString( "report.l10n.ok" );
309 String totalLabel = bundle.getString( "report.l10n.total" );
310 String additionalKeysLabel = bundle.getString( "report.l10n.additional" );
311 String nontranslatedKeysLabel = bundle.getString( "report.l10n.nontranslated" );
312 String[] headers = new String[locales != null ? locales.size() + 2 : 2];
313 Map localeDisplayNames = new HashMap();
314 headers[0] = pathColumnName;
315 headers[1] = defaultLocaleColumnName;
316 if ( locales != null )
317 {
318 Iterator it = locales.iterator();
319 int ind = 2;
320 while ( it.hasNext() )
321 {
322 final String localeCode = (String) it.next();
323 headers[ind] = localeCode;
324 ind = ind + 1;
325
326 Locale locale = createLocale( localeCode );
327 if ( locale == null )
328 {
329
330 localeDisplayNames.put( localeCode, localeCode );
331 }
332 else
333 {
334 localeDisplayNames.put( localeCode, locale.getDisplayName( rendererLocale ) );
335 }
336 }
337 }
338 tableHeader( headers );
339 int[] count = new int[locales != null ? locales.size() + 1 : 1];
340 Arrays.fill( count, 0 );
341 Iterator it = files.iterator();
342 MavenProject lastPrj = null;
343 Set usedFiles = new TreeSet( new WrapperComparator() );
344 while ( it.hasNext() )
345 {
346 Wrapper wr = (Wrapper) it.next();
347 if ( reactorProjects.size() > 1 && ( lastPrj == null || lastPrj != wr.getProject() ) )
348 {
349 lastPrj = wr.getProject();
350 sink.tableRow();
351 String name = wr.getProject().getName();
352 if ( name == null )
353 {
354 name = wr.getProject().getGroupId() + ":" + wr.getProject().getArtifactId();
355 }
356 tableCell( "<b><i>" + name + "</b></i>", true );
357 sink.tableRow_();
358 }
359 if ( wr.getFile().getName().endsWith( ".properties" )
360 && !localedPattern.matcher( wr.getFile().getName() ).matches() )
361 {
362 usedFiles.add( wr );
363 sink.tableRow();
364 tableCell( wr.getPath() );
365 Properties props = new Properties();
366 BufferedInputStream in = null;
367 try
368 {
369 in = new BufferedInputStream( new FileInputStream( wr.getFile() ) );
370 props.load( in );
371 wr.getProperties().put( Wrapper.DEFAULT_LOCALE, props );
372 tableCell( "" + props.size(), true );
373 count[0] = count[0] + props.size();
374 if ( locales != null )
375 {
376 Iterator it2 = locales.iterator();
377 int i = 1;
378 while ( it2.hasNext() )
379 {
380 String loc = (String) it2.next();
381 String nm = wr.getFile().getName();
382 String fn = nm.substring( 0, nm.length() - ".properties".length() );
383 File locFile = new File( wr.getFile().getParentFile(), fn + "_" + loc + ".properties" );
384 if ( locFile.exists() )
385 {
386 BufferedInputStream in2 = null;
387 Properties props2 = new Properties();
388 try
389 {
390 in2 = new BufferedInputStream( new FileInputStream( locFile ) );
391 props2.load( in2 );
392 wr.getProperties().put( loc, props2 );
393 Set missing = new HashSet( props.keySet() );
394 missing.removeAll( props2.keySet() );
395 Set additional = new HashSet( props2.keySet() );
396 additional.removeAll( props.keySet() );
397 Set nonTranslated = new HashSet();
398 Iterator itx = props.keySet().iterator();
399 while ( itx.hasNext() )
400 {
401 String k = (String) itx.next();
402 String val1 = props.getProperty( k );
403 String val2 = props2.getProperty( k );
404 if ( val2 != null && val1.equals( val2 ) )
405 {
406 nonTranslated.add( k );
407 }
408 }
409 count[i] = count[i] + ( props.size() - missing.size() - nonTranslated.size() );
410 StringBuffer statusRows = new StringBuffer();
411 if ( missing.size() != 0 )
412 {
413 statusRows.append( "<tr><td>" + missingKeysLabel + "</td><td><b>"
414 + missing.size() + "</b></td></tr>" );
415 }
416 else
417 {
418 statusRows.append( "<tr><td> </td><td> </td></tr>" );
419 }
420 if ( additional.size() != 0 )
421 {
422 statusRows.append( "<tr><td>" + additionalKeysLabel + "</td><td><b>"
423 + additional.size() + "</b></td></tr>" );
424 }
425 else
426 {
427 statusRows.append( "<tr><td> </td><td> </td></tr>" );
428 }
429 if ( nonTranslated.size() != 0 )
430 {
431 statusRows.append( "<tr><td>" + nontranslatedKeysLabel + "</td><td><b>"
432 + nonTranslated.size() + "</b></td></tr>" );
433 }
434 tableCell( wrapInTable( okLabel, statusRows.toString() ), true );
435 }
436 finally
437 {
438 IOUtil.close( in2 );
439 }
440 }
441 else
442 {
443 tableCell( missingFileLabel );
444 count[i] = count[i] + 0;
445 }
446 i = i + 1;
447 }
448 }
449 }
450 catch ( IOException ex )
451 {
452 getLog().error( ex );
453 }
454 finally
455 {
456 IOUtil.close( in );
457 }
458 sink.tableRow_();
459 }
460 }
461 sink.tableRow();
462 tableCell( totalLabel );
463 for ( int i = 0; i < count.length; i++ )
464 {
465 if ( i != 0 && count[0] != 0 )
466 {
467 tableCell( "<b>" + count[i] + "</b><br />(" + ( count[i] * 100 / count[0] ) + " %)", true );
468 }
469 else if ( i == 0 )
470 {
471 tableCell( "<b>" + count[i] + "</b>", true );
472 }
473 }
474 sink.tableRow_();
475
476 endTable();
477 sink.paragraph();
478 text( bundle.getString( "report.l10n.legend" ) );
479 sink.paragraph_();
480 sink.list();
481 sink.listItem();
482 text( bundle.getString( "report.l10n.list1" ) );
483 sink.listItem_();
484 sink.listItem();
485 text( bundle.getString( "report.l10n.list2" ) );
486 sink.listItem_();
487 sink.listItem();
488 text( bundle.getString( "report.l10n.list3" ) );
489 sink.listItem_();
490 sink.list_();
491 sink.paragraph();
492 text( bundle.getString( "report.l10n.note" ) );
493 sink.paragraph_();
494 endSection();
495
496 if ( locales != null )
497 {
498 Iterator itx = locales.iterator();
499 sink.list();
500 while ( itx.hasNext() )
501 {
502 String x = (String) itx.next();
503 sink.listItem();
504 link( "#" + x, x + " - " + localeDisplayNames.get( x ) );
505 sink.listItem_();
506 }
507 sink.list_();
508
509 itx = locales.iterator();
510 while ( itx.hasNext() )
511 {
512 String x = (String) itx.next();
513 startSection( x + " - " + localeDisplayNames.get( x ) );
514 sink.anchor( x );
515 sink.anchor_();
516 startTable();
517 tableCaption( bundle.getString( "report.l10n.locale" ) + " " + localeDisplayNames.get( x ) );
518 tableHeader( new String[] {bundle.getString( "report.l10n.tableheader1" ),
519 bundle.getString( "report.l10n.tableheader2" ),
520 bundle.getString( "report.l10n.tableheader3" ),
521 bundle.getString( "report.l10n.tableheader4" )} );
522 Iterator usedIter = usedFiles.iterator();
523 while ( usedIter.hasNext() )
524 {
525 sink.tableRow();
526 Wrapper wr = (Wrapper) usedIter.next();
527 tableCell( wr.getPath() );
528 Properties defs = (Properties) wr.getProperties().get( Wrapper.DEFAULT_LOCALE );
529 Properties locals = (Properties) wr.getProperties().get( x );
530 if ( locals == null )
531 {
532 locals = new Properties();
533 }
534 Set missing = new TreeSet( defs.keySet() );
535 missing.removeAll( locals.keySet() );
536 String cell = "";
537 Iterator ms = missing.iterator();
538 while ( ms.hasNext() )
539 {
540 cell = cell + "<tr><td>" + ms.next() + "</td></tr>";
541 }
542 tableCell( wrapInTable( okLabel, cell ), true );
543 Set additional = new TreeSet( locals.keySet() );
544 additional.removeAll( defs.keySet() );
545 Iterator ex = additional.iterator();
546 cell = "";
547 while ( ex.hasNext() )
548 {
549 cell = cell + "<tr><td>" + ex.next() + "</td></tr>";
550 }
551 tableCell( wrapInTable( okLabel, cell ), true );
552 Set nonTranslated = new TreeSet();
553 Iterator itnt = defs.keySet().iterator();
554 while ( itnt.hasNext() )
555 {
556 String k = (String) itnt.next();
557 String val1 = defs.getProperty( k );
558 String val2 = locals.getProperty( k );
559 if ( val2 != null && val1.equals( val2 ) )
560 {
561 nonTranslated.add( k );
562 }
563 }
564 Iterator nt = nonTranslated.iterator();
565 cell = "";
566 while ( nt.hasNext() )
567 {
568 String n = (String) nt.next();
569 cell = cell + "<tr><td>" + n + "</td><td>\"" + defs.getProperty( n ) + "\"</td></tr>";
570 }
571 tableCell( wrapInTable( okLabel, cell ), true );
572
573 sink.tableRow_();
574 }
575 endTable();
576 endSection();
577 }
578 }
579 endSection();
580 }
581
582
583
584
585
586
587
588 private Locale createLocale( String localeCode )
589 {
590
591 String[] localeComponents = StringUtils.split( localeCode, "_" );
592 Locale locale = null;
593 if ( localeComponents.length == 1 )
594 {
595 locale = new Locale( localeComponents[0] );
596 }
597 else if ( localeComponents.length == 2 )
598 {
599 locale = new Locale( localeComponents[0], localeComponents[1] );
600 }
601 else if ( localeComponents.length == 3 )
602 {
603 locale = new Locale( localeComponents[0], localeComponents[1], localeComponents[2] );
604 }
605 return locale;
606 }
607
608 private String wrapInTable( String okLabel, String cell )
609 {
610 if ( cell.length() == 0 )
611 {
612 cell = okLabel;
613 }
614 else
615 {
616 cell = "<table><tbody>" + cell + "</tbody></table>";
617 }
618 return cell;
619 }
620 }
621
622 private static class Wrapper
623 {
624
625 private String path;
626
627 private File file;
628
629 private MavenProject proj;
630
631 private Map properties;
632
633 static final String DEFAULT_LOCALE = "Default";
634
635 public Wrapper( String p, File f, MavenProject prj )
636 {
637 path = p;
638 file = f;
639 proj = prj;
640 properties = new HashMap();
641 }
642
643 public File getFile()
644 {
645 return file;
646 }
647
648
649 public String getPath()
650 {
651 return path;
652 }
653
654 public MavenProject getProject()
655 {
656 return proj;
657 }
658
659 public Map getProperties()
660 {
661 return properties;
662 }
663
664 }
665
666 private static class WrapperComparator
667 implements Comparator
668 {
669
670 public int compare( Object o1, Object o2 )
671 {
672 Wrapper wr1 = (Wrapper) o1;
673 Wrapper wr2 = (Wrapper) o2;
674 int comp1 = wr1.getProject().getBasedir().compareTo( wr2.getProject().getBasedir() );
675 if ( comp1 != 0 )
676 {
677 return comp1;
678 }
679 return wr1.getFile().compareTo( wr2.getFile() );
680 }
681
682 }
683 }