package org.codehaus.mojo.l10n; /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.maven.doxia.sink.Sink; import org.apache.maven.doxia.siterenderer.Renderer; import org.apache.maven.model.Resource; import org.apache.maven.project.MavenProject; import org.apache.maven.reporting.AbstractMavenReport; import org.apache.maven.reporting.AbstractMavenReportRenderer; import org.apache.maven.reporting.MavenReportException; import org.codehaus.plexus.util.DirectoryScanner; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.StringUtils; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; import java.util.TreeSet; import java.util.regex.Pattern; /** * A simple report for keeping track of l10n status. It lists all bundle properties * files and the number of properties in them. For a configurable list of locales it also * tracks the progress of localization. * * @author <a href="mkleint@codehaus.org">Milos Kleint</a> * @goal report */ public class L10NStatusReport extends AbstractMavenReport { /** * Report output directory. * * @parameter default-value="${project.build.directory}/generated-site/xdoc" */ private File outputDirectory; /** * Doxia Site Renderer. * * @component */ private Renderer siteRenderer; /** * A list of locale strings that are to be watched for l10n status. * * @parameter */ private List locales; /** * The Maven Project. * * @parameter expression="${project}" * @required * @readonly */ private MavenProject project; /** * The list of resources that are scanned for properties bundles. * * @parameter default-value="${project.resources}" * @readonly */ private List resources; /** * A list of exclude patterns to use. By default no files are excluded. * * @parameter */ private List excludes; /** * A list of include patterns to use. By default all <code>*.properties</code> files are included. * * @parameter */ private List includes; /** * The projects in the reactor for aggregation report. * * @parameter expression="${reactorProjects}" * @readonly */ protected List reactorProjects; /** * Whether to build an aggregated report at the root, or build individual reports. * * @parameter expression="${maven.l10n.aggregate}" default-value="false" */ protected boolean aggregate; private static final String[] DEFAULT_INCLUDES = {"**/*.properties"}; private static final String[] EMPTY_STRING_ARRAY = {}; /** * @see org.apache.maven.reporting.AbstractMavenReport#getSiteRenderer() */ protected Renderer getSiteRenderer() { return siteRenderer; } /** * @see org.apache.maven.reporting.AbstractMavenReport#getOutputDirectory() */ protected String getOutputDirectory() { return outputDirectory.getAbsolutePath(); } /** * @see org.apache.maven.reporting.AbstractMavenReport#getProject() */ protected MavenProject getProject() { return project; } public boolean canGenerateReport() { return canGenerateReport( constructResourceDirs() ); } /** * @param sourceDirs * @return true if the report can be generated */ protected boolean canGenerateReport( Map sourceDirs ) { boolean canGenerate = !sourceDirs.isEmpty(); if ( aggregate && !project.isExecutionRoot() ) { canGenerate = false; } return canGenerate; } /** * Collects resource definitions from all projects in reactor. * * @return */ protected Map constructResourceDirs() { Map sourceDirs = new HashMap(); if ( aggregate ) { for ( Iterator i = reactorProjects.iterator(); i.hasNext(); ) { MavenProject prj = (MavenProject) i.next(); if ( prj.getResources() != null && !prj.getResources().isEmpty() ) { sourceDirs.put( prj, new ArrayList( prj.getResources() ) ); } } } else { if ( resources != null && !resources.isEmpty() ) { sourceDirs.put( project, new ArrayList( resources ) ); } } return sourceDirs; } /** * @see org.apache.maven.reporting.AbstractMavenReport#executeReport(java.util.Locale) */ protected void executeReport( Locale locale ) throws MavenReportException { Set included = new TreeSet( new WrapperComparator() ); Map res = constructResourceDirs(); for ( Iterator it = res.keySet().iterator(); it.hasNext(); ) { MavenProject prj = (MavenProject) it.next(); List lst = (List) res.get( prj ); for ( Iterator i = lst.iterator(); i.hasNext(); ) { Resource resource = (Resource) i.next(); File resourceDirectory = new File( resource.getDirectory() ); if ( !resourceDirectory.exists() ) { getLog().info( "Resource directory does not exist: " + resourceDirectory ); continue; } DirectoryScanner scanner = new DirectoryScanner(); scanner.setBasedir( resource.getDirectory() ); List allIncludes = new ArrayList(); if ( resource.getIncludes() != null && !resource.getIncludes().isEmpty() ) { allIncludes.addAll( resource.getIncludes() ); } if ( includes != null && !includes.isEmpty() ) { allIncludes.addAll( includes ); } if ( allIncludes.isEmpty() ) { scanner.setIncludes( DEFAULT_INCLUDES ); } else { scanner.setIncludes( (String[]) allIncludes.toArray( EMPTY_STRING_ARRAY ) ); } List allExcludes = new ArrayList(); if ( resource.getExcludes() != null && !resource.getExcludes().isEmpty() ) { allExcludes.addAll( resource.getExcludes() ); } else if ( excludes != null && !excludes.isEmpty() ) { allExcludes.addAll( excludes ); } scanner.setExcludes( (String[]) allExcludes.toArray( EMPTY_STRING_ARRAY ) ); scanner.addDefaultExcludes(); scanner.scan(); List includedFiles = Arrays.asList( scanner.getIncludedFiles() ); for ( Iterator j = includedFiles.iterator(); j.hasNext(); ) { String name = (String) j.next(); File source = new File( resource.getDirectory(), name ); included.add( new Wrapper( name, source, prj ) ); } } } // Write the overview L10NStatusRenderer r = new L10NStatusRenderer( getSink(), getBundle( locale ), included, locale ); r.render(); } /** * @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale) */ public String getDescription( Locale locale ) { return getBundle( locale ).getString( "report.l10n.description" ); } /** * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale) */ public String getName( Locale locale ) { return getBundle( locale ).getString( "report.l10n.name" ); } /** * @see org.apache.maven.reporting.MavenReport#getOutputName() */ public String getOutputName() { return "l10n-status"; } private static ResourceBundle getBundle( Locale locale ) { return ResourceBundle.getBundle( "l10n-status-report", locale, L10NStatusReport.class.getClassLoader() ); } /** * Generates an overview page with a list of properties bundles * and a link to each locale's status. */ class L10NStatusRenderer extends AbstractMavenReportRenderer { private final ResourceBundle bundle; /** * The locale in which the report will be rendered. */ private final Locale rendererLocale; private Set files; private Pattern localedPattern = Pattern.compile( ".*_[a-zA-Z]{2}[_]?[a-zA-Z]{0,2}?\\.properties" ); public L10NStatusRenderer( Sink sink, ResourceBundle bundle, Set files, Locale rendererLocale ) { super( sink ); this.bundle = bundle; this.files = files; this.rendererLocale = rendererLocale; } /** * @see org.apache.maven.reporting.MavenReportRenderer#getTitle() */ public String getTitle() { return bundle.getString( "report.l10n.title" ); } /** * @see org.apache.maven.reporting.AbstractMavenReportRenderer#renderBody() */ public void renderBody() { startSection( getTitle() ); paragraph( bundle.getString( "report.l10n.intro" ) ); startSection( bundle.getString( "report.l10n.summary" ) ); startTable(); tableCaption( bundle.getString( "report.l10n.summary.caption" ) ); String defaultLocaleColumnName = bundle.getString( "report.l10n.column.default" ); String pathColumnName = bundle.getString( "report.l10n.column.path" ); String missingFileLabel = bundle.getString( "report.l10n.missingFile" ); String missingKeysLabel = bundle.getString( "report.l10n.missingKey" ); String okLabel = bundle.getString( "report.l10n.ok" ); String totalLabel = bundle.getString( "report.l10n.total" ); String additionalKeysLabel = bundle.getString( "report.l10n.additional" ); String nontranslatedKeysLabel = bundle.getString( "report.l10n.nontranslated" ); String[] headers = new String[locales != null ? locales.size() + 2 : 2]; Map localeDisplayNames = new HashMap(); headers[0] = pathColumnName; headers[1] = defaultLocaleColumnName; if ( locales != null ) { Iterator it = locales.iterator(); int ind = 2; while ( it.hasNext() ) { final String localeCode = (String) it.next(); headers[ind] = localeCode; ind = ind + 1; Locale locale = createLocale( localeCode ); if ( locale == null ) { // If the localeCode were in an unknown format use the localeCode itself as a fallback value localeDisplayNames.put( localeCode, localeCode ); } else { localeDisplayNames.put( localeCode, locale.getDisplayName( rendererLocale ) ); } } } tableHeader( headers ); int[] count = new int[locales != null ? locales.size() + 1 : 1]; Arrays.fill( count, 0 ); Iterator it = files.iterator(); MavenProject lastPrj = null; Set usedFiles = new TreeSet( new WrapperComparator() ); while ( it.hasNext() ) { Wrapper wr = (Wrapper) it.next(); if ( reactorProjects.size() > 1 && ( lastPrj == null || lastPrj != wr.getProject() ) ) { lastPrj = wr.getProject(); sink.tableRow(); String name = wr.getProject().getName(); if ( name == null ) { name = wr.getProject().getGroupId() + ":" + wr.getProject().getArtifactId(); } tableCell( "<b><i>" + name + "</b></i>", true ); sink.tableRow_(); } if ( wr.getFile().getName().endsWith( ".properties" ) && !localedPattern.matcher( wr.getFile().getName() ).matches() ) { usedFiles.add( wr ); sink.tableRow(); tableCell( wr.getPath() ); Properties props = new Properties(); BufferedInputStream in = null; try { in = new BufferedInputStream( new FileInputStream( wr.getFile() ) ); props.load( in ); wr.getProperties().put( Wrapper.DEFAULT_LOCALE, props ); tableCell( "" + props.size(), true ); count[0] = count[0] + props.size(); if ( locales != null ) { Iterator it2 = locales.iterator(); int i = 1; while ( it2.hasNext() ) { String loc = (String) it2.next(); String nm = wr.getFile().getName(); String fn = nm.substring( 0, nm.length() - ".properties".length() ); File locFile = new File( wr.getFile().getParentFile(), fn + "_" + loc + ".properties" ); if ( locFile.exists() ) { BufferedInputStream in2 = null; Properties props2 = new Properties(); try { in2 = new BufferedInputStream( new FileInputStream( locFile ) ); props2.load( in2 ); wr.getProperties().put( loc, props2 ); Set missing = new HashSet( props.keySet() ); missing.removeAll( props2.keySet() ); Set additional = new HashSet( props2.keySet() ); additional.removeAll( props.keySet() ); Set nonTranslated = new HashSet(); Iterator itx = props.keySet().iterator(); while ( itx.hasNext() ) { String k = (String) itx.next(); String val1 = props.getProperty( k ); String val2 = props2.getProperty( k ); if ( val2 != null && val1.equals( val2 ) ) { nonTranslated.add( k ); } } count[i] = count[i] + ( props.size() - missing.size() - nonTranslated.size() ); StringBuffer statusRows = new StringBuffer(); if ( missing.size() != 0 ) { statusRows.append( "<tr><td>" + missingKeysLabel + "</td><td><b>" + missing.size() + "</b></td></tr>" ); } else { statusRows.append( "<tr><td> </td><td> </td></tr>" ); } if ( additional.size() != 0 ) { statusRows.append( "<tr><td>" + additionalKeysLabel + "</td><td><b>" + additional.size() + "</b></td></tr>" ); } else { statusRows.append( "<tr><td> </td><td> </td></tr>" ); } if ( nonTranslated.size() != 0 ) { statusRows.append( "<tr><td>" + nontranslatedKeysLabel + "</td><td><b>" + nonTranslated.size() + "</b></td></tr>" ); } tableCell( wrapInTable( okLabel, statusRows.toString() ), true ); } finally { IOUtil.close( in2 ); } } else { tableCell( missingFileLabel ); count[i] = count[i] + 0; } i = i + 1; } } } catch ( IOException ex ) { getLog().error( ex ); } finally { IOUtil.close( in ); } sink.tableRow_(); } } sink.tableRow(); tableCell( totalLabel ); for ( int i = 0; i < count.length; i++ ) { if ( i != 0 && count[0] != 0 ) { tableCell( "<b>" + count[i] + "</b><br />(" + ( count[i] * 100 / count[0] ) + " %)", true ); } else if ( i == 0 ) { tableCell( "<b>" + count[i] + "</b>", true ); } } sink.tableRow_(); endTable(); sink.paragraph(); text( bundle.getString( "report.l10n.legend" ) ); sink.paragraph_(); sink.list(); sink.listItem(); text( bundle.getString( "report.l10n.list1" ) ); sink.listItem_(); sink.listItem(); text( bundle.getString( "report.l10n.list2" ) ); sink.listItem_(); sink.listItem(); text( bundle.getString( "report.l10n.list3" ) ); sink.listItem_(); sink.list_(); sink.paragraph(); text( bundle.getString( "report.l10n.note" ) ); sink.paragraph_(); endSection(); if ( locales != null ) { Iterator itx = locales.iterator(); sink.list(); while ( itx.hasNext() ) { String x = (String) itx.next(); sink.listItem(); link( "#" + x, x + " - " + localeDisplayNames.get( x ) ); sink.listItem_(); } sink.list_(); itx = locales.iterator(); while ( itx.hasNext() ) { String x = (String) itx.next(); startSection( x + " - " + localeDisplayNames.get( x ) ); sink.anchor( x ); sink.anchor_(); startTable(); tableCaption( bundle.getString( "report.l10n.locale" ) + " " + localeDisplayNames.get( x ) ); tableHeader( new String[]{ bundle.getString( "report.l10n.tableheader1" ), bundle.getString( "report.l10n.tableheader2" ), bundle.getString( "report.l10n.tableheader3" ), bundle.getString( "report.l10n.tableheader4" ) } ); Iterator usedIter = usedFiles.iterator(); while ( usedIter.hasNext() ) { sink.tableRow(); Wrapper wr = (Wrapper) usedIter.next(); tableCell( wr.getPath() ); Properties defs = (Properties) wr.getProperties().get( Wrapper.DEFAULT_LOCALE ); Properties locals = (Properties) wr.getProperties().get( x ); if ( locals == null ) { locals = new Properties(); } Set missing = new TreeSet( defs.keySet() ); missing.removeAll( locals.keySet() ); String cell = ""; Iterator ms = missing.iterator(); while ( ms.hasNext() ) { cell = cell + "<tr><td>" + ms.next() + "</td></tr>"; } tableCell( wrapInTable( okLabel, cell ), true ); Set additional = new TreeSet( locals.keySet() ); additional.removeAll( defs.keySet() ); Iterator ex = additional.iterator(); cell = ""; while ( ex.hasNext() ) { cell = cell + "<tr><td>" + ex.next() + "</td></tr>"; } tableCell( wrapInTable( okLabel, cell ), true ); Set nonTranslated = new TreeSet(); Iterator itnt = defs.keySet().iterator(); while ( itnt.hasNext() ) { String k = (String) itnt.next(); String val1 = defs.getProperty( k ); String val2 = locals.getProperty( k ); if ( val2 != null && val1.equals( val2 ) ) { nonTranslated.add( k ); } } Iterator nt = nonTranslated.iterator(); cell = ""; while ( nt.hasNext() ) { String n = (String) nt.next(); cell = cell + "<tr><td>" + n + "</td><td>\"" + defs.getProperty( n ) + "\"</td></tr>"; } tableCell( wrapInTable( okLabel, cell ), true ); sink.tableRow_(); } endTable(); endSection(); } } endSection(); } /** * Take the supplied locale code, split into its different parts and create a Locale object from it. * * @param localeCode The code for a locale in the format language[_country[_variant]] * @return A suitable Locale object, ot <code>null</code> if the code was in an unknown format */ private Locale createLocale( String localeCode ) { // Split the localeCode into language/country/variant String[] localeComponents = StringUtils.split( localeCode, "_" ); Locale locale = null; if ( localeComponents.length == 1 ) { locale = new Locale( localeComponents[0] ); } else if ( localeComponents.length == 2 ) { locale = new Locale( localeComponents[0], localeComponents[1] ); } else if ( localeComponents.length == 3 ) { locale = new Locale( localeComponents[0], localeComponents[1], localeComponents[2] ); } return locale; } private String wrapInTable( String okLabel, String cell ) { if ( cell.length() == 0 ) { cell = okLabel; } else { cell = "<table><tbody>" + cell + "</tbody></table>"; } return cell; } } private static class Wrapper { private String path; private File file; private MavenProject proj; private Map properties; static final String DEFAULT_LOCALE = "Default"; public Wrapper( String p, File f, MavenProject prj ) { path = p; file = f; proj = prj; properties = new HashMap(); } public File getFile() { return file; } public String getPath() { return path; } public MavenProject getProject() { return proj; } public Map getProperties() { return properties; } } private static class WrapperComparator implements Comparator { public int compare( Object o1, Object o2 ) { Wrapper wr1 = (Wrapper) o1; Wrapper wr2 = (Wrapper) o2; int comp1 = wr1.getProject().getBasedir().compareTo( wr2.getProject().getBasedir() ); if ( comp1 != 0 ) { return comp1; } return wr1.getFile().compareTo( wr2.getFile() ); } } }