View Javadoc
1   package org.codehaus.mojo.l10n;
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.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   * A simple report for keeping track of l10n status. It lists all bundle properties
55   * files and the number of properties in them. For a configurable list of locales it also
56   * tracks the progress of localization.
57   *
58   * @author <a href="mkleint@codehaus.org">Milos Kleint</a>
59   * @since 1.0.0
60   */
61  @Mojo( name = "report" )
62  public class L10NStatusReport
63      extends AbstractMavenReport
64  {
65      /**
66       * The projects in the reactor for aggregation report.
67       *
68       * @since 1.0.0
69       */
70      @Parameter( defaultValue = "${reactorProjects}", readonly = true )
71      protected List<MavenProject> reactorProjects;
72  
73      /**
74       * A list of locale strings that are to be watched for l10n status.
75       *
76       * @since 1.0.0
77       */
78      @Parameter
79      private List<String> locales;
80  
81      /**
82       * A list of exclude patterns to use. By default no files are excluded.
83       *
84       * @since 1.0.0
85       */
86      @Parameter
87      private List<String> excludes;
88  
89      /**
90       * A list of include patterns to use. By default, all <code>*.properties</code> files are included.
91       *
92       * @since 1.0.0
93       */
94      @Parameter
95      private List<String> includes;
96  
97      /**
98       * Whether to build an aggregated report at the root, or build individual reports.
99       *
100      * @since 1.0.0
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      * Collects resource definitions from all projects in reactor.
122      *
123      * @return
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      * @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale)
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         // Write the overview
223         L10NStatusRenderer r = new L10NStatusRenderer( getSink(), getBundle( locale ), included, locale );
224         r.render();
225     }
226 
227     /**
228      * @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale)
229      */
230     public String getDescription( Locale locale )
231     {
232         return getBundle( locale ).getString( "report.l10n.description" );
233     }
234 
235     /**
236      * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale)
237      */
238     public String getName( Locale locale )
239     {
240         return getBundle( locale ).getString( "report.l10n.name" );
241     }
242 
243     /**
244      * @see org.apache.maven.reporting.MavenReport#getOutputName()
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      * Generates an overview page with a list of properties bundles
258      * and a link to each locale's status.
259      */
260     class L10NStatusRenderer
261         extends AbstractMavenReportRenderer
262     {
263 
264         private final ResourceBundle bundle;
265 
266         /**
267          * The locale in which the report will be rendered.
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          * @see org.apache.maven.reporting.MavenReportRenderer#getTitle()
286          */
287         public String getTitle()
288         {
289             return bundle.getString( "report.l10n.title" );
290         }
291 
292         /**
293          * @see org.apache.maven.reporting.AbstractMavenReportRenderer#renderBody()
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                         // If the localeCode were in an unknown format use the localeCode itself as a fallback value
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>&nbsp;</td><td>&nbsp;</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>&nbsp;</td><td>&nbsp;</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] ) + "&nbsp;%)", 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          * Take the supplied locale code, split into its different parts and create a Locale object from it.
584          *
585          * @param localeCode The code for a locale in the format language[_country[_variant]]
586          * @return A suitable Locale object, ot <code>null</code> if the code was in an unknown format
587          */
588         private Locale createLocale( String localeCode )
589         {
590             // Split the localeCode into language/country/variant
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 }