com.numdata.oss.junit.ResourceBundleTester Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of numdata-commons Show documentation
Show all versions of numdata-commons Show documentation
Miscellaneous basic Java tools.
/*
* Copyright (c) 2017, Numdata BV, The Netherlands.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Numdata nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL NUMDATA BV BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.numdata.oss.junit;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.Map.*;
import java.util.regex.*;
import com.numdata.oss.*;
import com.numdata.oss.ensemble.*;
import com.numdata.oss.io.*;
import org.jetbrains.annotations.*;
import org.junit.*;
/**
* JUnit unit tool class to help with testing resource bundles. Note that this
* class can be used as a:
*
*
*
* - Stand-alone application. Reads bundles from the
* command-line arguments and tests them for errors. Command-line options can be
* added to change the test behaviour.
*
* - JUnit test helper. Call {@code testBundles()} from your
* test case to ensure resource bundle presence and content during the unit test
* phase.
*
* - A utility class. Instances of this class provide the
* same functionality as with the previous two uses to other classes.
*
*
*
* Checks include resource bundle syntax and consistency amongst bundles.
* Consistency checks include keys, whitespace, alignment, special characters,
* and comments.
*
* TODO Check if values in different bundles are duplicates. Difficulty would be
* finding out when duplicates are acceptable...
*
* @author D. van 't Oever
* @author Peter S. Heijnen
*/
public class ResourceBundleTester
{
/**
* Pattern for a resource bundle name.
*/
private static final Pattern BUNDLE_NAME_PATTERN = Pattern.compile( "^(.*?)(_([a-z][a-z])(_([A-Z][A-Z])(_([a-zA-Z_)]+))?)?)?\\.(?i)properties$" );
/**
* Log used to keep track of errors that occurred when comparing the
* resource bundles.
*/
private final PrintWriter _log;
/**
* Contents of each loaded resource bundle, mapped by (file) name.
*/
private final SortedMap> _bundlesByName = new TreeMap>();
/**
* Generate warnings concirning comment lines in resource bundles.
*/
private boolean _commentWarnings = true;
/**
* Generate warnings concerning whitespace in resource bundles.
*/
private boolean _whitespaceWarnings = true;
/**
* Generate warnings concerning empty values in resource bundles.
*/
private boolean _emptyValueWarnings = true;
/**
* Find differences between different locales of each bundle, i.e. missing
* keys.
*/
private boolean _findLocaleDifferences = false;
/**
* When set, a warnings are given for entries that have the same value for
* different locales.
*/
private boolean _sameValueInDifferentLocale = false;
/**
* Process input files recursively.
*/
private boolean _recursive = false;
/**
* Input files to be processed.
*/
private final List _inputFiles;
/**
* Whether to generate verbose output.
*/
private boolean _verbose = false;
/**
* Run application.
*
* @param args Command-line arguments.
*/
public static void main( final String[] args )
{
boolean ok = true;
final PrintWriter log = new PrintWriter( System.out, true );
final ResourceBundleTester tester = new ResourceBundleTester( log );
if ( args.length == 0 )
{
ok = false;
}
try
{
final Queue arguments = new LinkedList( Arrays.asList( args ) );
while ( !arguments.isEmpty() )
{
final String argument = arguments.peek();
if ( TextTools.startsWith( argument, '-' ) )
{
arguments.remove();
if ( argument.length() == 1 )
{
break;
}
for ( int i = 1; i < argument.length(); i++ )
{
switch ( argument.charAt( i ) )
{
case 'c':
tester.setCommentWarnings( false );
break;
case 'l':
tester.setFindLocaleDifferences( true );
break;
case 'd':
tester.setFindLocaleDifferences( true );
tester.setSameValueInDifferentLocale( true );
break;
// case 'm': // TODO Support merging of resource bundles or something useful like that. Need to figure out what is actually useful...
// final String locale = arguments.poll();
// if ( locale == null )
// {
// System.err.println( "Missing argument for option -m." );
// ok = false;
// }
// else
// {
// tester.setMergeLocale( TextTools.parseLocale( locale ) );
// }
// break;
case 'r':
tester.setRecursive( true );
break;
case 'v':
tester.setVerbose( true );
break;
case 'w':
tester.setWhitespaceWarnings( false );
break;
case 'e':
tester.setEmptyValueWarnings( false );
break;
default:
System.err.println( "Unknown option: " + argument );
ok = false;
break;
}
}
}
else if ( argument.startsWith( "--" ) )
{
arguments.remove();
final String argumentName = argument.substring( 2 );
if ( "verbose".equals( argumentName ) )
{
tester.setVerbose( true );
}
else
{
System.err.println( "Unknown option: " + argument );
}
}
else
{
break;
}
}
tester.setInputFiles( arguments );
if ( !ok )
{
// 80 characters: 12345678901234567890123456789012345678901234567890123456789012345678901234567890
System.out.println( "Usage:" );
System.out.println( " [OPTION]... resource-bundle..." );
System.out.println();
System.out.println( "Options:" );
System.out.println( " -c Ignore comments." );
System.out.println( " -e Ignore empty values." );
System.out.println( " -w Ignore whitespace." );
System.out.println( " -r Process subdirectories recursively." );
System.out.println( " -l Check for differences between locales." );
System.out.println( " -d Check for identical values in different locales. Implies -l." );
System.out.println( " -m LOCALE Merge, i.e. copy missing entries from specified locale." );
System.out.println( " -v, --verbose Verbose output." );
}
else
{
System.out.println( "Finished with exit value: " + tester.call() );
}
}
catch ( final FileNotFoundException e )
{
log.println( "error: resource bundle not found (" + e.getMessage() + ')' );
ok = false;
}
catch ( final IOException e )
{
log.println( "error: I/O exception, message: " + e.getMessage() );
ok = false;
}
if ( !ok )
{
System.out.println( "Failed!" );
}
}
/**
* Test resource bundles for a class.
*
* @param forClass Class to test resource bundles for.
* @param includeHierarchy {@code true} to include resource bundles for
* super-classes.
* @param minimumLocales List of minimum required locales.
* @param allowLocaleDiffs Allow differences between locales;
* @param expectedKeys List of keys that are required in all bundles.
* @param allowUnknown Allow unknown keys (not in expectedKeys) in
* bundles.
*
* @return List with unknown keys that were found in the resource bundle.
*/
public static List testBundles( @NotNull final Class> forClass, final boolean includeHierarchy, @NotNull final Collection minimumLocales, final boolean allowLocaleDiffs, @NotNull final Collection expectedKeys, final boolean allowUnknown )
{
return testBundles( forClass, includeHierarchy, minimumLocales.toArray( new Locale[ minimumLocales.size() ] ), allowLocaleDiffs, expectedKeys, allowUnknown, true, false );
}
/**
* Test resource bundles for a class.
*
* @param forClass Class to test resource bundles for.
* @param includeHierarchy {@code true} to include resource bundles for
* super-classes.
* @param minimumLocales List of minimum required locales.
* @param allowLocaleDiffs Allow differences between locales;
* @param expectedKeys List of keys that are required in all bundles.
* @param allowUnknown Allow unknown keys (not in expectedKeys) in
* bundles.
* @param allowNonAscii Allow non-ASCII characters (>127) in values.
* @param allowHTML Allow HTML tags in values.
*
* @return List with unknown keys that were found in the resource bundle.
*/
public static List testBundles( @NotNull final Class> forClass, final boolean includeHierarchy, @NotNull final Locale[] minimumLocales, final boolean allowLocaleDiffs, @Nullable final Collection expectedKeys, final boolean allowUnknown, final boolean allowNonAscii, final boolean allowHTML )
{
return testBundles( forClass, includeHierarchy, Arrays.asList( minimumLocales ), allowLocaleDiffs, ( expectedKeys != null ) ? expectedKeys : Collections.emptySet(), allowUnknown, allowNonAscii, allowHTML );
}
/**
* Test resource bundles for a class.
*
* @param forClass Class to test resource bundles for.
* @param includeHierarchy {@code true} to include resource bundles for
* super-classes.
* @param locales List of minimum required locales.
* @param allowLocaleDiffs Allow differences between locales;
* @param expectedKeys List of keys that are required in all bundles.
* @param allowUnknown Allow unknown keys (not in expectedKeys) in
* bundles.
* @param allowNonAscii Allow non-ASCII characters (>127) in values.
* @param allowHTML Allow HTML tags in values.
*
* @return List with unknown keys that were found in the resource bundle.
*/
public static List testBundles( @NotNull final Class> forClass, final boolean includeHierarchy, @NotNull final Collection locales, final boolean allowLocaleDiffs, @NotNull final Collection expectedKeys, final boolean allowUnknown, final boolean allowNonAscii, final boolean allowHTML )
{
System.out.println( " - class : " + forClass.getName() );
System.out.println( " - hierarchy : " + includeHierarchy );
System.out.println( " - tried locales : " + locales );
if ( !allowUnknown && expectedKeys.isEmpty() )
{
throw new Error( "error in test: no expected keys and no unknown keys allowed!?" );
}
final StringBuilder errors = new StringBuilder();
final Map bundles = new LinkedHashMap();
for ( final Locale locale : locales )
{
try
{
final ResourceBundle bundle = includeHierarchy ? ResourceBundleTools.getBundleHierarchy( forClass, locale ) : ResourceBundleTools.getBundle( forClass, locale );
bundles.put( bundle.getLocale(), bundle );
}
catch ( final MissingResourceException ignored )
{
errors.append( "\nBundle '" );
errors.append( forClass.getName() );
errors.append( "' not found for locale '" );
errors.append( locale );
errors.append( '\'' );
}
}
if ( errors.length() > 0 )
{
Assert.fail( "Error(s):" + errors );
}
System.out.println( " - found locales : " + bundles.keySet() );
return testBundles( bundles.values(), allowLocaleDiffs, expectedKeys, allowUnknown, allowNonAscii, allowHTML );
}
/**
* Test resource bundles for a class.
*
* @param baseName Base name of resource bundles to test.
* @param locales List of locales to test.
* @param allowLocaleDiffs Allow differences between locales;
* @param expectedKeys List of keys that are required in all bundles.
* @param allowUnknown Allow unknown keys (not in expectedKeys) in
* bundles.
* @param allowNonAscii Allow non-ASCII characters (>127) in values.
* @param allowHTML Allow HTML tags in values.
*
* @return List with unknown keys that were found in the resource bundle.
*/
public static List testBundles( @NotNull final String baseName, @NotNull final Collection locales, final boolean allowLocaleDiffs, @NotNull final Collection expectedKeys, final boolean allowUnknown, final boolean allowNonAscii, final boolean allowHTML )
{
System.out.println( " - baseName : " + baseName );
System.out.println( " - tried locales : " + locales );
if ( !allowUnknown && expectedKeys.isEmpty() )
{
throw new Error( "error in test: no expected keys and no unknown keys allowed!?" );
}
final StringBuilder errors = new StringBuilder();
final Map bundles = new LinkedHashMap();
for ( final Locale locale : locales )
{
try
{
final ResourceBundle bundle = ResourceBundleTools.getBundle( baseName, locale, null );
bundles.put( bundle.getLocale(), bundle );
}
catch ( final MissingResourceException ignored )
{
errors.append( "\nBundle '" );
errors.append( baseName );
errors.append( "' not found for locale '" );
errors.append( locale );
errors.append( '\'' );
}
}
if ( errors.length() > 0 )
{
Assert.fail( "Error(s):" + errors );
}
System.out.println( " - found locales : " + bundles.keySet() );
return testBundles( bundles.values(), allowLocaleDiffs, expectedKeys, allowUnknown, allowNonAscii, allowHTML );
}
/**
* Test resource bundles for a class.
*
* @param bundles Resource bundles to test.
* @param allowLocaleDiffs Allow differences between locales;
* @param expectedKeys List of keys that are required in all bundles.
* @param allowUnknown Allow unknown keys (not in expectedKeys) in
* bundles.
* @param allowNonAscii Allow non-ASCII characters (>127) in values.
* @param allowHTML Allow HTML tags in values.
*
* @return List with unknown keys that were found in the resource bundle.
*/
public static List testBundles( @NotNull final Collection bundles, final boolean allowLocaleDiffs, @NotNull final Collection expectedKeys, final boolean allowUnknown, final boolean allowNonAscii, final boolean allowHTML )
{
System.out.println( " - test options : allowLocaleDiffs=" + allowLocaleDiffs + ", allowUnknown=" + allowUnknown + ", allowNonAscii=" + allowNonAscii + ", allowHTML=" + allowHTML );
System.out.println( " - expectedKeys : " + expectedKeys );
if ( !allowUnknown && expectedKeys.isEmpty() )
{
throw new Error( "error in test: no expected keys and no unknown keys allowed!?" );
}
final StringBuilder errors = new StringBuilder();
final Collection seenLocales = new ArrayList( bundles.size() );
final List unknownKeys = new ArrayList();
for ( final ResourceBundle bundle : bundles )
{
Locale locale = bundle.getLocale();
if ( locale == null )
{
locale = Locale.ROOT;
}
final String bundleDesc = Locale.ROOT.equals( locale ) ? "default" : "'" + locale + '\'';
/*
* Check presence of expected keys
*/
for ( final String key : expectedKeys )
{
try
{
bundle.getString( key );
}
catch ( final MissingResourceException ignored )
{
errors.append( "\nMissing key '" );
errors.append( key );
errors.append( "' in " );
errors.append( bundleDesc );
errors.append( " bundle." );
}
}
if ( !allowLocaleDiffs && allowUnknown )
{
for ( final String key : unknownKeys )
{
if ( ResourceBundleTools.getString( bundle, key, null ) == null )
{
errors.append( "\nMissing key '" );
errors.append( key );
errors.append( "' in " );
errors.append( bundleDesc );
errors.append( " bundle." );
}
}
}
/*
* Check for presence of unknown keys.
*/
for ( final Enumeration keys = bundle.getKeys(); keys.hasMoreElements(); )
{
final String key = keys.nextElement();
final String keyDesc = "bundle '" + locale + ", key";
final String value = bundle.getString( key );
final String valueDesc = "resource '" + locale + '.' + key + "' value";
if ( !expectedKeys.contains( key ) && !unknownKeys.contains( key ) )
{
if ( !allowUnknown )
{
errors.append( "\nUnknown key '" );
errors.append( key );
errors.append( "' was found in " );
errors.append( bundleDesc );
errors.append( " bundle" );
}
else if ( !seenLocales.isEmpty() && !allowLocaleDiffs )
{
errors.append( "\nKey '" );
errors.append( key );
errors.append( "' was found in " );
errors.append( bundleDesc );
errors.append( " bundle but not for previous locales (" );
errors.append( seenLocales );
errors.append( ')' );
}
unknownKeys.add( key );
}
testNonAscii( errors, keyDesc, key );
if ( !allowNonAscii )
{
testNonAscii( errors, valueDesc, value );
}
if ( !allowHTML )
{
testHTML( errors, valueDesc, value );
}
}
seenLocales.add( locale );
}
if ( errors.length() > 0 )
{
Assert.fail( "Error(s):" + errors );
}
return unknownKeys;
}
/**
* Test if the supplied string contains any non-ASCII characters
* (8,9,10,12,13,27,32-127). Throws an exception if so.
*
* @param errors Buffer to write errors to.
* @param what Description of string being tested (used for error
* message).
* @param s String to test.
*
* @throws AssertionError if test fails.
* @see #testBundles
*/
public static void testNonAscii( @NotNull final StringBuilder errors, @NotNull final String what, @Nullable final String s )
{
if ( s != null )
{
for ( int i = 0; i < s.length(); i++ )
{
final char c = s.charAt( i );
if ( ( ( c < '\040' ) && ( c != '\b' ) && ( c != '\t' ) && ( c != '\n' ) && ( c != '\f' ) && ( c != '\r' ) && ( c != '\033' ) ) || ( c > '\177' ) )
{
errors.append( '\n' );
errors.append( what );
errors.append( " '" );
errors.append( s );
errors.append( "' contains non-ASCII character '" );
errors.append( c );
errors.append( "' (" );
errors.append( (int)c );
errors.append( ')' );
break;
}
}
}
}
/**
* Test if the supplied string contains any HTML characters (&....).
*
* @param errors Buffer to write errors to.
* @param what Description of string being tested (used for error
* message).
* @param s String to test.
*
* @throws AssertionError if test fails.
* @see #testBundles
*/
public static void testHTML( @NotNull final StringBuilder errors, @NotNull final String what, @Nullable final String s )
{
if ( s != null )
{
final int length = s.length();
int pos = 0;
while ( true )
{
pos = s.indexOf( '&', pos ) + 1;
if ( ( pos <= 0 ) || ( pos >= length ) )
{
break;
}
final char next = s.charAt( pos );
if ( Character.isLetterOrDigit( next ) )
{
errors.append( '\n' );
errors.append( what );
errors.append( " '" );
errors.append( s );
errors.append( "' contains HTML text: " );
errors.append( s, pos - 1, s.length() );
break;
}
}
}
}
/**
* Construct new ResourceBundleTester.
*
* @param log The used to write errors/warnings to.
*/
public ResourceBundleTester( final PrintWriter log )
{
_log = log;
_inputFiles = new ArrayList();
}
/**
* Sets the resource bundle files to be processed.
*
* @param inputFiles Resource bundle file names.
*/
public void setInputFiles( final Collection inputFiles )
{
_inputFiles.clear();
_inputFiles.addAll( inputFiles );
}
/**
* Returns whether warnings are given about inconsistent comments.
*
* @return {@code true} if comment warnings are given.
*/
public boolean isCommentWarnings()
{
return _commentWarnings;
}
/**
* Sets whether warnings are given about inconsistent comments.
*
* @param commentWarnings {@code true} to enable comment warnings.
*/
public void setCommentWarnings( final boolean commentWarnings )
{
_commentWarnings = commentWarnings;
}
/**
* Returns whether warnings are given about inconsistent whitespace.
*
* @return {@code true} if whitespace warnings are given.
*/
public boolean isWhitespaceWarnings()
{
return _whitespaceWarnings;
}
/**
* Sets whether warnings are given about inconsistent whitespace.
*
* @param whitespaceWarnings {@code true} to enable whitespace warnings.
*/
public void setWhitespaceWarnings( final boolean whitespaceWarnings )
{
_whitespaceWarnings = whitespaceWarnings;
}
/**
* Returns whether warnings are given about empty values.
*
* @return {@code true} if empty value warnings are given.
*/
public boolean isEmptyValueWarnings()
{
return _emptyValueWarnings;
}
/**
* Sets whether warnings are given about empty values.
*
* @param emptyValueWarnings {@code true} to enable empty value warnings.
*/
public void setEmptyValueWarnings( final boolean emptyValueWarnings )
{
_emptyValueWarnings = emptyValueWarnings;
}
/**
* Returns whether finding of differences between locales is enabled.
*
* @return {@code true} if finding differences between locales is enabled.
*/
public boolean isFindLocaleDifferences()
{
return _findLocaleDifferences;
}
/**
* Sets whether finding of differences between locales is enabled.
*
* @param findLocaleDifferences {@code true} to enable finding differences
* between locales.
*/
public void setFindLocaleDifferences( final boolean findLocaleDifferences )
{
_findLocaleDifferences = findLocaleDifferences;
}
/**
* Returns whether warnings are given for entries identical values in
* different locales.
*
* @return {@code true} if these warnings are enabled.
*/
public boolean isSameValueInDifferentLocale()
{
return _sameValueInDifferentLocale;
}
/**
* Sets whether warnings are given for entries identical values in different
* locales.
*
* @param sameValueInDifferentLocale {@code true} to enable these warnings.
*/
public void setSameValueInDifferentLocale( final boolean sameValueInDifferentLocale )
{
_sameValueInDifferentLocale = sameValueInDifferentLocale;
}
/**
* Returns whether directories are processed recursively.
*
* @return {@code true} if directories are processed recursively.
*/
public boolean isRecursive()
{
return _recursive;
}
/**
* Sets whether directories are processed recursively.
*
* @param recursive {@code true} to process directories recursively.
*/
public void setRecursive( final boolean recursive )
{
_recursive = recursive;
}
/**
* Returns whether verbose output will be generated.
*
* @return {@code true} if output is verbose.
*/
public boolean isVerbose()
{
return _verbose;
}
/**
* Sets whether verbose output will be generated.
*
* @param verbose {@code true} for verbose output.
*/
public void setVerbose( final boolean verbose )
{
_verbose = verbose;
}
/**
* Runs the resource bundle tester using currently set options and input
* files.
*
* @return Process exit value ({@code 0} when successful).
*
* @throws IOException if an I/O error occurs.
*/
public Integer call()
throws IOException
{
final BasicSolo result = new BasicSolo( Boolean.TRUE );
System.out.println( "Loading resource bundles..." );
for ( final String argument : _inputFiles )
{
final FileVisitor visitor = new FileVisitor()
{
@Override
public void visit( @NotNull final File file )
throws IOException
{
final String path = file.toString();
if ( isVerbose() )
{
_log.println( "Loading " + path + "..." );
}
final FileReader r = new FileReader( file );
try
{
result.setValue( load( r, path ) );
}
finally
{
r.close();
}
}
};
FileTools.visitRecursively( new File( argument ), new FileExtensionFilter( ".properties", null, false ), visitor );
}
if ( result.getValue() )
{
if ( isFindLocaleDifferences() )
{
findLocaleDifferences();
}
// result.setValue( Boolean.valueOf( check() ) );
}
return result.getValue() ? 0 : -1;
}
/**
* Creates a model of the given resource bundle and checks for tabs.
*
* @param sourceReader The reader that will be used to read characters from
* the given resource bundle.
* @param bundleName Identifies the bundle for the user.
*
* @return {@code true} if bundle was loaded successfully; {@code false} if
* the bundle contains errors.
*
* @throws IOException if the was an I/O problem while loading the bundle.
*/
public boolean load( final Reader sourceReader, final String bundleName )
throws IOException
{
final StringBuilder sb = new StringBuilder();
final List lines = new ArrayList();
int lastLineBeginPosition = 0;
int currentLine = 1;
char lastChar = '\0';
while ( true )
{
final int i = sourceReader.read();
if ( i < 0 )
{
break;
}
final char c = (char)i;
if ( c == '\t' ) // Tab encountered
{
if ( isWhitespaceWarnings() )
{
_log.println( bundleName + ": WARNING: Line " + currentLine + ": Tab character found." );
}
}
else if ( c == '\n' ) // End of line encountered
{
boolean isContinuation = false;
if ( !lines.isEmpty() )
{
final Line lastLine = lines.get( lines.size() - 1 );
if ( TextTools.endsWith( lastLine.getValue(), '\\' ) )
{
isContinuation = true;
}
}
final String lineString = sb.substring( lastLineBeginPosition, sb.length() );
final Line line = new Line( bundleName, lineString, currentLine, isContinuation );
currentLine++;
lastLineBeginPosition = sb.length();
lines.add( line );
// System.out.println( "Line: " + lineString );
}
else if ( c < ' ' )
{
_log.println( bundleName + ": ERROR: Line " + currentLine + ": Control character(" + (int)c + ") found." );
}
else
{
sb.append( c );
}
lastChar = c;
}
/*
* Perform checks on single bundle.
*/
final boolean success;
if ( lastChar != '\n' )
{
if ( isWhitespaceWarnings() )
{
_log.println( bundleName + ": WARNING: Line " + currentLine + ": No newline at end of file." );
}
boolean isContinuation = false;
if ( !lines.isEmpty() )
{
final Line lastLine = lines.get( lines.size() - 1 );
if ( TextTools.endsWith( lastLine.getValue(), '\\' ) )
{
isContinuation = true;
}
}
final String lineString = sb.substring( lastLineBeginPosition, sb.length() );
final Line line = new Line( bundleName, lineString, currentLine, isContinuation );
lines.add( line );
success = true;
}
else
{
success = checkAlignment( lines, bundleName );
}
if ( success )
{
_bundlesByName.put( bundleName, lines );
}
return success;
}
/**
* Finds and reports any differences between different localizations of the
* same resource bundle.
*/
private void findLocaleDifferences()
{
System.out.println( "Finding differences between locales..." );
String currentBaseName = null;
final Map> currentBundles = new LinkedHashMap>();
for ( final Map.Entry> entry : _bundlesByName.entrySet() )
{
final Matcher matcher = BUNDLE_NAME_PATTERN.matcher( entry.getKey() );
if ( matcher.matches() )
{
final String baseName = matcher.group( 1 );
if ( currentBaseName == null )
{
currentBaseName = baseName;
}
else if ( !baseName.equals( currentBaseName ) )
{
findLocaleDifferences( currentBaseName, currentBundles );
currentBundles.clear();
currentBaseName = baseName;
}
final String language = matcher.group( 3 );
final String country = matcher.group( 5 );
final String variant = matcher.group( 7 );
final Locale locale = new Locale( ( language == null ) ? "" : language, ( country == null ) ? "" : country, ( variant == null ) ? "" : variant );
currentBundles.put( locale, entry.getValue() );
}
}
if ( !currentBundles.isEmpty() && ( currentBaseName != null ) )
{
findLocaleDifferences( currentBaseName, currentBundles );
}
}
/**
* Searches the given bundles for differences.
*
* @param baseName Base name of the bundles.
* @param bundles Map with different translations of the same logical
* resource bundle, by locale.
*/
private void findLocaleDifferences( @NotNull final String baseName, @NotNull final Map> bundles )
{
final Set>> bundleEntries = bundles.entrySet();
final Iterator>> bundleIterator = bundleEntries.iterator();
final Map.Entry> firstBundle = bundleIterator.next();
final Locale firstLocale = firstBundle.getKey();
final List firstLines = firstBundle.getValue();
final Map firstContent = getLogicalBundleContent( firstLines );
while ( bundleIterator.hasNext() )
{
final Map.Entry> otherBundle = bundleIterator.next();
final Locale otherLocale = otherBundle.getKey();
final List otherLines = otherBundle.getValue();
final Map otherContent = getLogicalBundleContent( otherLines );
final Map difference = new LinkedHashMap( otherContent );
final Set differenceKeys = difference.keySet();
for ( final Entry entry : firstContent.entrySet() )
{
final String key = entry.getKey();
if ( differenceKeys.remove( key ) )
{
if ( isSameValueInDifferentLocale() )
{
// Common key
final BundleEntry firstEntry = entry.getValue();
final BundleEntry otherEntry = otherContent.get( firstEntry.getKey() );
final String firstValue = firstEntry.getValue();
final String otherValue = otherEntry.getValue();
if ( firstValue.equals( otherValue ) )
{
System.out.println( baseName + ": Same value for '" + firstEntry.getKey() + "' in locales '" + firstLocale + "' and '" + otherLocale + '\'' );
}
}
}
else
{
// Only in first bundle
System.out.println( baseName + ": Entry for '" + key + "' present in locale '" + firstLocale + "' but not in locale '" + otherLocale + '\'' );
}
}
for ( final String key : difference.keySet() )
{
// Only in other bundle
System.out.println( baseName + ": Entry for '" + key + "' present in locale '" + otherLocale + "' but not in locale '" + firstLocale + '\'' );
}
}
}
/**
* Processes the given resource bundle lines into resource bundle entries,
* which represent the parsed contents associated with a single key. By
* contrast, a {@code Line} represents a single line from the input file and
* may contain only part of a resource bundle entry, due to escaped line
* breaks.
*
* @param lines Lines read from the resource bundle file.
*
* @return Bundle entries derived from the given lines.
*/
private Map getLogicalBundleContent( final Iterable lines )
{
final Map result = new HashMap();
BundleEntry currentEntry = null;
for ( final Line line : lines )
{
if ( line.getType() == Line.CONTINUATION )
{
if ( currentEntry != null )
{
currentEntry.append( line.getValue() );
}
}
else
{
if ( currentEntry != null )
{
result.put( currentEntry.getKey(), currentEntry );
currentEntry = null;
}
if ( line.getType() == Line.RESOURCE )
{
final String key = line.getKey();
currentEntry = new BundleEntry( line.getLineNumber(), key.trim(), line.getValue() );
}
}
}
if ( currentEntry != null )
{
result.put( currentEntry.getKey(), currentEntry );
}
return result;
}
/**
* Represents a single entry from a resource bundle.
*/
private static class BundleEntry
{
/**
* Line number where the entry starts.
*/
private final int _lineNumber;
/**
* Key identifying the entry.
*/
private final String _key;
/**
* Value of the entry.
*/
private String _value;
/**
* Constructs a new resource bundle entry.
*
* @param lineNumber Line number where the entry starts.
* @param key Key identifying the entry.
* @param value Value of the entry.
*/
private BundleEntry( final int lineNumber, final String key, final String value )
{
_lineNumber = lineNumber;
_key = key;
_value = value;
}
/**
* Returns the entry's key.
*
* @return Key identifying the entry.
*/
public String getKey()
{
return _key;
}
/**
* Returns the entry's value.
*
* @return Value of the entry.
*/
public String getValue()
{
return _value;
}
/**
* Appends the given string to the entry's current value.
*
* @param value String to be appended.
*/
public void append( final String value )
{
_value += value;
}
/**
* Returns the line number where the entry starts.
*
* @return Entry's line number.
*/
public int getLineNumber()
{
return _lineNumber;
}
}
/**
* Check all loaded resource bundles.
*
* @return {@code true} when all loaded bundles are correct; {@code false}
* when one or more bundles are inconsistent.
*/
public boolean check()
{
return checkKeys( _bundlesByName.entrySet() );
}
/**
* Compares the keys of the given resource bundles against the first
* resource bundle in the list.
*
* @param bundles Bundles to be checked.
*
* @return {@code True} when the keys (including spaces!) are identical.
*/
private boolean checkKeys( @NotNull final Iterable>> bundles )
{
boolean result = true;
final Iterator>> bundleIterator = bundles.iterator();
/*
* Iterate through all resource bundle models.
*/
final Map.Entry> sourceBundle = bundleIterator.next();
while ( bundleIterator.hasNext() )
{
final Map.Entry> destBundle = bundleIterator.next();
final String sourceName = sourceBundle.getKey();
final String destName = destBundle.getKey();
final List sourceList = sourceBundle.getValue();
final List destList = destBundle.getValue();
/*
* Check each line of the first resource bundle against the
* corresponding line in the other bundles.
*/
for ( int j = 0; j < sourceList.size() && j < destList.size(); j++ )
{
final Line sourceLine = sourceList.get( j );
final Line destLine = destList.get( j );
if ( sourceLine.getType() == Line.COMMENT )
{
if ( destLine.getType() != Line.COMMENT )
{
if ( isCommentWarnings() )
{
_log.println( destBundle + ": No corresponding comment line in resource bundle '" + destName + "' for comment: " + sourceLine.getText() + " at line " + sourceLine.getLineNumber() );
result = false;
}
}
else if ( !TextTools.equals( sourceLine.getText(), destLine.getText() ) )
{
if ( isCommentWarnings() )
{
_log.println( destBundle + ": Comment at line " + sourceLine.getLineNumber() + " doesn't match resource bundle " + sourceName );
result = false;
}
}
}
else if ( sourceLine.getType() == Line.RESOURCE && destLine.getType() == Line.RESOURCE )
{
final String sourceKey = sourceLine.getKey();
final String destKey = destLine.getKey();
if ( !sourceKey.equals( destKey ) )
{
final String trimmedSourceKey = sourceKey.trim();
if ( trimmedSourceKey.equals( destKey.trim() ) )
{
if ( isWhitespaceWarnings() )
{
_log.println( "warning: Whitespace around key: '" + trimmedSourceKey + "' at line " + sourceLine.getLineNumber() + " does not match with whitespace around corresponding key in resource bundle " + destName );
}
}
else
{
_log.println( "error: Key '" + trimmedSourceKey + "' at line " + sourceLine.getLineNumber() + " does not match with '" + destKey.trim() + "' in resource bundle " + destName );
}
result = false;
}
}
} // end for
} // end for
return result;
}
/**
* Checks if lines are aligned correctly in a resource bundle. Multi-line
* values are checked as follows: A multi-line value should look like:
*
*
*
* buttonText = Click on this\
*
* button!
*
*
*
* The space before 'button!' should be the same as the position before
* 'Click'.
*
* The alignment of '=' characters in blocks of key/value pairs is checked
* as well. (In a block they should all be on the same position)
*
* @param lines The model of a resource bundle.
* @param bundleName Identifies the bundle for the user.
*
* @return {@code true} when all lines (if any) are aligned correctly;
* otherwise {@code false}
*/
public boolean checkAlignment( final Iterable lines, final String bundleName )
{
final int[] equalsPositionInBlock = new int[ getNumberOfBlocks( lines ) ];
boolean success = true;
int blockNr = 0;
int alignmentPosition = 0;
for ( final Line line : lines )
{
if ( line.getType() == Line.EMPTY || line.getType() == Line.COMMENT )
{
blockNr++;
}
if ( line.getType() == Line.CONTINUATION )
{
// System.out.println( "ha: " + prevLine.getKey() + " ." );
final String lineText = line.getValue();
final String trimmedLineText = lineText.trim();
final int startPosition = lineText.indexOf( trimmedLineText.charAt( 0 ) );
if ( isWhitespaceWarnings() && ( alignmentPosition != startPosition ) )
{
_log.println( bundleName + ": WARNING: The continuation at " + line.getLineNumber() + " in resource bundle '" + bundleName + "' is not correctly aligned (pos=" + startPosition + ", should be " + alignmentPosition + ")." );
success = false;
}
// System.out.println( "endcontinuation" );
}
if ( line.getType() == Line.RESOURCE )
{
final String lineText = line.getText();
final int equalsPos = lineText.indexOf( '=' );
if ( equalsPositionInBlock[ blockNr ] == 0 )
{
equalsPositionInBlock[ blockNr ] = equalsPos;
}
if ( equalsPositionInBlock[ blockNr ] != equalsPos )
{
if ( isWhitespaceWarnings() )
{
_log.println( bundleName + ": WARNING: '=' at position " + equalsPositionInBlock[ blockNr ] + " in block " + blockNr + " in resource bundle '" + bundleName + "' is not aligned correctly compared to the first line '" + equalsPos + "' of the block" );
}
}
final String key = line.getKey();
alignmentPosition = key.length() + 2;
}
}
return success;
}
/**
* Get bean property names.
*
* @param beanClass Bean class.
* @param setterRequired Require setter for properties.
* @param excludeProperties Property name to exclude.
*
* @return Bean property names.
*/
public static Set getBeanPropertyNames( final Class> beanClass, final boolean setterRequired, final String... excludeProperties )
{
final Set result = new HashSet();
final Collection excludeSet = new HashSet( Arrays.asList( excludeProperties ) );
final Method[] methods = beanClass.getMethods();
final Collection methodNames = new HashSet();
for ( final Method method : methods )
{
methodNames.add( method.getName() );
}
for ( final Method method : methods )
{
final String methodName = method.getName();
final String basename;
if ( methodName.startsWith( "get" ) )
{
basename = methodName.substring( 3 );
}
else if ( methodName.startsWith( "is" ) )
{
basename = methodName.substring( 2 );
}
else
{
basename = null;
}
if ( ( basename != null ) && ( !setterRequired || methodNames.contains( "set" + basename ) ) )
{
final String propertyName = TextTools.decapitalize( basename );
if ( !excludeSet.contains( propertyName ) )
{
result.add( propertyName );
}
}
}
return result;
}
/**
* Get the number of key/value blocks in a given resource bundle. A serie of
* key/values is considered a new block after an empty or a comment line.
*
* @param list The resource bundle that will be counted for blocks.
*
* @return The number of key/value blocks that appeared in the given
* resource bundle.
*/
public static int getNumberOfBlocks( final Iterable list )
{
int nr = 1;
for ( final Line line : list )
{
if ( line.getType() == Line.EMPTY || line.getType() == Line.COMMENT )
{
nr++;
}
}
return nr;
}
/**
* Represents one line of a resource bundle.
*/
private class Line
{
/**
* Constant indicating that a line is a comment line.
*/
public static final int COMMENT = 0;
/**
* Constant indicating that a line is a resource line.
*/
public static final int RESOURCE = 1;
/**
* Constant indicating that a line is empty ("").
*/
public static final int EMPTY = 2;
/**
* Constant indicating that a line is a continuation of the previous
* line.
*/
public static final int CONTINUATION = 3;
/**
* String that holds the line.
*/
private final String _line;
/**
* The linenumber this line was found on.
*/
private final int _lineNumber;
/**
* The key element of this line, including whitespace until the "="
* character! {@code null} when this line is not of type RESOURCE.
*/
private final String _key;
/**
* The value element of this line. {@code null} when this line is of
* type COMMENT.
*/
private final String _value;
/**
* The type of this line. (COMMENT or RESOURCE).
*/
private final int _type;
/**
* Constructs a new line from the specified resource bundle.
*
* @param bundleName Name of the resource bundle.
* @param line The text that makes up this line.
* @param lineNumber The number this line was found on.
* @param continuation Indicates whether this line is a continuation of
* a previous line ({@code True}) or not ({@code
* false}).
*/
private Line( final String bundleName, final String line, final int lineNumber, final boolean continuation )
{
_line = line;
_lineNumber = lineNumber;
final PrintWriter log = _log;
final String trimmed = _line.trim();
if ( trimmed.isEmpty() )
{
_type = EMPTY;
_key = null;
_value = _line;
}
else if ( trimmed.charAt( 0 ) == '#' )
{
_type = COMMENT;
_key = null;
_value = _line;
}
else if ( _line.contains( "=" ) )
{
_type = RESOURCE;
_key = _line.substring( 0, _line.indexOf( '=' ) );
_value = _line.substring( _line.indexOf( '=' ) + 1, _line.length() );
}
else if ( continuation )
{
_type = CONTINUATION;
_key = null;
_value = _line;
}
else
{
log.println( bundleName + ": ERROR: Text: '" + _line + "' at line " + lineNumber + " is illegal (forgot '\\' at the end of previous line?)" );
_type = -1;
_key = null;
_value = _line;
}
if ( _type == RESOURCE )
{
/*
* Perform some checks
*/
if ( _value.isEmpty() )
{
if ( isEmptyValueWarnings() )
{
log.println( bundleName + ": WARNING: Empty value at line " + lineNumber );
}
}
else if ( _value.charAt( 0 ) != ' ' )
{
if ( isWhitespaceWarnings() )
{
log.println( bundleName + ": WARNING: No space after '=' at line " + lineNumber );
}
}
// @FIXME what is this supposed to do????
// else if ( _value.length() > 1 && _value.charAt( 1 ) == ' ' )
// {
// log.println( "warning: No space, followed by non-space detected after '=' for key: " + _key.trim() + " at line " + lineNumber );
// }
if ( _line.contains( "?" ) && ( _line.indexOf( '?' ) != _line.length() - 1 ) )
{
log.println( bundleName + ": WARNING: ? detected inside line: " + lineNumber );
}
}
}
/**
* Get the {@code String} that represents this line.
*
* @return {@code String} representing this line.
*/
public String getText()
{
return _line;
}
/**
* Get the number this line is on.
*
* @return The number this line is on.
*/
public int getLineNumber()
{
return _lineNumber;
}
/**
* Get the key of this line.
*
* @return {@code String} containing the key of this line. Includins
* whitespace until the "=" character! {@code null} when this line is
* not of type RESOURCE.
*/
public String getKey()
{
return _key;
}
/**
* Get the value of this line.
*
* @return {@code String} containing the value of this line. When this
* line is op type RESOURCE, it contains the value after the '=' sign,
* otherwise it is the same as getLine().
*/
public String getValue()
{
return _value;
}
/**
* Returns the type of this line.
*
* @return The type of this line (COMMENT, RESOURCE, EMPTY or
* CONTINUATION).
*/
public int getType()
{
return _type;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy