diff --git a/src/main/java/org/codehaus/mojo/properties/AbstractWritePropertiesMojo.java b/src/main/java/org/codehaus/mojo/properties/AbstractWritePropertiesMojo.java index d1277d9..1db8eeb 100644 --- a/src/main/java/org/codehaus/mojo/properties/AbstractWritePropertiesMojo.java +++ b/src/main/java/org/codehaus/mojo/properties/AbstractWritePropertiesMojo.java @@ -32,6 +32,7 @@ import java.io.PrintWriter; import java.io.StringReader; import java.io.StringWriter; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -44,6 +45,10 @@ public abstract class AbstractWritePropertiesMojo extends AbstractMojo { + /** + * Default encoding for the output file. Package private for testing. + */ + static final String DEFAULT_ENCODING = "ISO-8859-1"; @Parameter( defaultValue = "${project}", required = true, readonly = true ) private MavenProject project; @@ -51,6 +56,41 @@ public abstract class AbstractWritePropertiesMojo @Parameter( required = true, property = "properties.outputFile" ) private File outputFile; + /** + * The encoding to use when writing the properties file. + */ + @Parameter( required = false, defaultValue = DEFAULT_ENCODING ) + private String encoding = DEFAULT_ENCODING; + + /** + * Default scope for test access. + * + * @param project The test project. + */ + void setProject( MavenProject project ) + { + this.project = project; + } + + /** + * Default scope for test access. + * + * @param encoding to write the output file in + */ + void setEncoding( String encoding ) + { + this.encoding = encoding; + } + + /** + * Default scope for test access + * + * @param outputFile the outputFile to set + */ + void setOutputFile(File outputFile) { + this.outputFile = outputFile; + } + /** * @param properties {@link Properties} * @param file {@link File} @@ -59,6 +99,8 @@ public abstract class AbstractWritePropertiesMojo protected void writeProperties( Properties properties, File file ) throws MojoExecutionException { + getLog().debug( String.format( "Writing properties to %s using encoding %s", file.toString(), this.encoding ) ); + try { storeWithoutTimestamp( properties, file, "Properties" ); @@ -79,7 +121,7 @@ protected void writeProperties( Properties properties, File file ) private void storeWithoutTimestamp( Properties properties, File outputFile, String comments ) throws IOException { - try ( PrintWriter pw = new PrintWriter( outputFile, "ISO-8859-1" ); StringWriter sw = new StringWriter() ) + try ( PrintWriter pw = new PrintWriter( outputFile, this.encoding ); StringWriter sw = new StringWriter() ) { properties.store( sw, comments ); comments = '#' + comments; @@ -122,6 +164,21 @@ protected void validateOutputFile() } } + /** + * @throws MojoExecutionException {@link MojoExecutionException} + */ + protected void validateEncoding() + throws MojoExecutionException + { + try + { + Charset.forName(this.encoding); + } + catch(IllegalArgumentException e) + { + throw new MojoExecutionException(String.format("Invalid encoding '%s'", this.encoding), e); + } + } /** * @return {@link MavenProject} */ diff --git a/src/main/java/org/codehaus/mojo/properties/ReadPropertiesMojo.java b/src/main/java/org/codehaus/mojo/properties/ReadPropertiesMojo.java index 30d3fd4..4f4d0fc 100644 --- a/src/main/java/org/codehaus/mojo/properties/ReadPropertiesMojo.java +++ b/src/main/java/org/codehaus/mojo/properties/ReadPropertiesMojo.java @@ -24,8 +24,10 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URL; +import java.nio.charset.Charset; import java.util.Enumeration; import java.util.Properties; @@ -50,6 +52,11 @@ public class ReadPropertiesMojo extends AbstractMojo { + /** + * Default encoding for the input properties file/url. Package private for testing. + */ + static final String DEFAULT_ENCODING = "ISO-8859-1"; + @Parameter( defaultValue = "${project}", readonly = true, required = true ) private MavenProject project; @@ -122,6 +129,16 @@ public void setKeyPrefix( String keyPrefix ) @Parameter( defaultValue = "false", property = "prop.skipLoadProperties" ) private boolean skipLoadProperties; + /** + * The encoding of the properties files. + */ + @Parameter( required = false, defaultValue = DEFAULT_ENCODING ) + private String encoding = DEFAULT_ENCODING; + + void setEncoding ( String encoding ) { + this.encoding = encoding; + } + /** * Used for resolving property placeholders. */ @@ -152,6 +169,14 @@ private void checkParameters() throw new MojoExecutionException( "Set files or URLs but not both - otherwise " + "no order of precedence can be guaranteed" ); } + try + { + Charset.forName(this.encoding); + } + catch(IllegalArgumentException e) + { + throw new MojoExecutionException(String.format("Invalid encoding '%s'", this.encoding), e); + } } private void loadFiles() @@ -190,23 +215,26 @@ private void loadProperties( Resource resource ) { try { - getLog().debug( "Loading properties from " + resource ); + getLog().debug( String.format( "Loading properties from %s using encoding %s", resource.toString(), this.encoding ) ); try ( InputStream stream = resource.getInputStream() ) { - if ( keyPrefix != null ) + try (InputStreamReader streamReader = new InputStreamReader( stream, this.encoding ) ) { - Properties properties = new Properties(); - properties.load( stream ); - Properties projectProperties = project.getProperties(); - for ( String key : properties.stringPropertyNames() ) + if ( keyPrefix != null ) { - projectProperties.put( keyPrefix + key, properties.get( key ) ); + Properties properties = new Properties(); + properties.load( streamReader ); + Properties projectProperties = project.getProperties(); + for ( String key : properties.stringPropertyNames() ) + { + projectProperties.put( keyPrefix + key, properties.get( key ) ); + } + } + else + { + project.getProperties().load( streamReader ); } - } - else - { - project.getProperties().load( stream ); } } } diff --git a/src/main/java/org/codehaus/mojo/properties/WriteActiveProfileProperties.java b/src/main/java/org/codehaus/mojo/properties/WriteActiveProfileProperties.java index d1f75f8..794d3ef 100644 --- a/src/main/java/org/codehaus/mojo/properties/WriteActiveProfileProperties.java +++ b/src/main/java/org/codehaus/mojo/properties/WriteActiveProfileProperties.java @@ -41,6 +41,7 @@ public void execute() throws MojoExecutionException { validateOutputFile(); + validateEncoding(); List list = getProject().getActiveProfiles(); if ( getLog().isInfoEnabled() ) { diff --git a/src/main/java/org/codehaus/mojo/properties/WriteProjectProperties.java b/src/main/java/org/codehaus/mojo/properties/WriteProjectProperties.java index 2bfff15..2c33d32 100644 --- a/src/main/java/org/codehaus/mojo/properties/WriteProjectProperties.java +++ b/src/main/java/org/codehaus/mojo/properties/WriteProjectProperties.java @@ -60,6 +60,7 @@ public void execute() throws MojoExecutionException { validateOutputFile(); + validateEncoding(); Properties projProperties = new Properties(); projProperties.putAll( getProject().getProperties() ); diff --git a/src/test/java/org/codehaus/mojo/properties/ReadPropertiesMojoTest.java b/src/test/java/org/codehaus/mojo/properties/ReadPropertiesMojoTest.java index 9cdaa4c..2899d05 100644 --- a/src/test/java/org/codehaus/mojo/properties/ReadPropertiesMojoTest.java +++ b/src/test/java/org/codehaus/mojo/properties/ReadPropertiesMojoTest.java @@ -1,23 +1,36 @@ package org.codehaus.mojo.properties; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; import java.util.Properties; +import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.project.MavenProject; import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; - public class ReadPropertiesMojoTest { private static final String NEW_LINE = System.getProperty( "line.separator" ); + private static final Map ISO_8859_REPRESENTABLE = Map.of( "test.property1", "value1®", + "test.property2", "value2" ); + + private static final Map ISO_8859_NOT_REPRESENTABLE = Map.of ( "test.property3", "value3™", + "test.property4", "κόσμε" ); + private MavenProject projectStub; private ReadPropertiesMojo readPropertiesMojo; @@ -32,14 +45,21 @@ public void setUp() @Test public void readPropertiesWithoutKeyprefix() throws Exception { - try ( FileReader fr = new FileReader( getPropertyFileForTesting() ) ) + final Map propMap = new HashMap<>(); + propMap.putAll( ISO_8859_REPRESENTABLE ); + propMap.putAll( ISO_8859_NOT_REPRESENTABLE ); + + final String encoding = ReadPropertiesMojo.DEFAULT_ENCODING; + File referenceFile = getPropertyFileForTesting( encoding, propMap ); + try ( FileInputStream fileStream = new FileInputStream( referenceFile ); + InputStreamReader fr = new InputStreamReader( fileStream, encoding ) ) { // load properties directly for comparison later Properties testProperties = new Properties(); testProperties.load( fr ); // do the work - readPropertiesMojo.setFiles( new File[] {getPropertyFileForTesting()} ); + readPropertiesMojo.setFiles( new File[] {getPropertyFileForTesting( encoding, propMap )} ); readPropertiesMojo.execute(); // check results @@ -51,6 +71,22 @@ public void readPropertiesWithoutKeyprefix() throws Exception // we are not adding prefix, so properties should be same as in file assertEquals( testProperties.size(), projectProperties.size() ); assertEquals( testProperties, projectProperties ); + + // strings which are representable in ISO-8859-1 should match the source data + for ( String sourceKey : ISO_8859_REPRESENTABLE.keySet() ) + { + assertEquals( ISO_8859_REPRESENTABLE.get( sourceKey ), projectProperties.getProperty( sourceKey ) ); + } + + // strings which are not representable in ISO-8859-1 underwent a conversion which lost data + for ( String sourceKey : ISO_8859_NOT_REPRESENTABLE.keySet() ) + { + String sourceValue = ISO_8859_NOT_REPRESENTABLE.get( sourceKey ); + String converted = new String( sourceValue.getBytes( encoding ), StandardCharsets.UTF_8 ); + String propVal = projectProperties.getProperty( sourceKey ); + assertNotEquals( sourceValue, propVal ); + assertEquals( converted, propVal ); + } } } @@ -58,21 +94,28 @@ public void readPropertiesWithoutKeyprefix() throws Exception public void readPropertiesWithKeyprefix() throws Exception { String keyPrefix = "testkey-prefix."; + final String encoding = ReadPropertiesMojo.DEFAULT_ENCODING; - try ( FileReader fs1 = new FileReader( getPropertyFileForTesting( keyPrefix ) ); - FileReader fs2 = new FileReader( getPropertyFileForTesting() ) ) + final Map propMap = new HashMap<>(); + propMap.putAll( ISO_8859_REPRESENTABLE ); + propMap.putAll( ISO_8859_NOT_REPRESENTABLE ); + + try ( FileInputStream fs1 = new FileInputStream( getPropertyFileForTesting( keyPrefix, encoding, propMap ) ); + FileInputStream fs2 = new FileInputStream( getPropertyFileForTesting( encoding, propMap ) ); + InputStreamReader fr1 = new InputStreamReader( fs1, encoding ); + InputStreamReader fr2 = new InputStreamReader( fs2, encoding ) ) { Properties testPropertiesWithoutPrefix = new Properties(); - testPropertiesWithoutPrefix.load( fs2 ); + testPropertiesWithoutPrefix.load( fr2 ); // do the work readPropertiesMojo.setKeyPrefix( keyPrefix ); - readPropertiesMojo.setFiles( new File[] {getPropertyFileForTesting()} ); + readPropertiesMojo.setFiles( new File[] {getPropertyFileForTesting( encoding, propMap )} ); readPropertiesMojo.execute(); // load properties directly and add prefix for comparison later Properties testPropertiesPrefix = new Properties(); - testPropertiesPrefix.load( fs1 ); + testPropertiesPrefix.load( fr1 ); // check results Properties projectProperties = projectStub.getProperties(); @@ -87,35 +130,96 @@ public void readPropertiesWithKeyprefix() throws Exception // properties with and without prefix shouldn't be same assertNotEquals( testPropertiesPrefix, testPropertiesWithoutPrefix ); assertNotEquals( testPropertiesWithoutPrefix, projectProperties ); + + // strings which are representable in ISO-8859-1 should match the source data + for ( String sourceKey : ISO_8859_REPRESENTABLE.keySet() ) + { + assertEquals( ISO_8859_REPRESENTABLE.get( sourceKey ), projectProperties.getProperty( keyPrefix + sourceKey ) ); + } + + // strings which are not representable in ISO-8859-1 underwent a conversion which lost data + for ( String sourceKey : ISO_8859_NOT_REPRESENTABLE.keySet() ) + { + String sourceValue = ISO_8859_NOT_REPRESENTABLE.get( sourceKey ); + String converted = new String( sourceValue.getBytes( encoding ), StandardCharsets.UTF_8 ); + String propVal = projectProperties.getProperty( keyPrefix + sourceKey ); + assertNotEquals( sourceValue, propVal ); + assertEquals( converted, propVal ); + } + } + } + + @Test + public void readPropertiesWithEncoding() throws Exception + { + final Map propMap = new HashMap<>(); + propMap.putAll( ISO_8859_REPRESENTABLE ); + propMap.putAll( ISO_8859_NOT_REPRESENTABLE ); + + final String encoding = StandardCharsets.UTF_8.name(); + File referenceFile = getPropertyFileForTesting( encoding, propMap ); + try ( FileInputStream fileStream = new FileInputStream( referenceFile ); + InputStreamReader fr = new InputStreamReader( fileStream, encoding ) ) + { + // load properties directly for comparison later + Properties testProperties = new Properties(); + testProperties.load( fr ); + + // do the work + readPropertiesMojo.setFiles( new File[] {getPropertyFileForTesting( encoding, propMap )} ); + readPropertiesMojo.setEncoding( encoding ); + readPropertiesMojo.execute(); + + // check results + Properties projectProperties = projectStub.getProperties(); + assertNotNull( projectProperties ); + // it should not be empty + assertNotEquals( 0, projectProperties.size() ); + + // we are not adding prefix, so properties should be same as in file + assertEquals( testProperties.size(), projectProperties.size() ); + assertEquals( testProperties, projectProperties ); + + // all test values are representable in UTF-8 and should match original source data + for ( String sourceKey : propMap.keySet() ) + { + assertEquals( propMap.get( sourceKey ), projectProperties.getProperty( sourceKey ) ); + } } } - private File getPropertyFileForTesting() throws IOException + @Test + public void testInvalidEncoding() throws Exception { - return getPropertyFileForTesting( null ); + readPropertiesMojo.setFiles( new File[] {getPropertyFileForTesting( StandardCharsets.UTF_8.name(), ISO_8859_REPRESENTABLE )} ); + readPropertiesMojo.setEncoding( "invalid-encoding" ); + MojoExecutionException thrown = assertThrows( MojoExecutionException.class, () -> readPropertiesMojo.execute() ); + assertEquals( thrown.getMessage(), "Invalid encoding 'invalid-encoding'" ); } - private File getPropertyFileForTesting( String keyPrefix ) throws IOException + private File getPropertyFileForTesting( String encoding, Map properties ) throws IOException + { + return getPropertyFileForTesting( null, encoding, properties ); + } + + private File getPropertyFileForTesting( String keyPrefix, String encoding, Map properties ) throws IOException { File f = File.createTempFile( "prop-test", ".properties" ); f.deleteOnExit(); - FileWriter writer = new FileWriter( f ); - String prefix = keyPrefix; - if ( prefix == null ) - { - prefix = ""; - } - try + try (FileOutputStream fileStream = new FileOutputStream( f ); // + OutputStreamWriter writer = new OutputStreamWriter( fileStream, encoding ) ) { - writer.write( prefix + "test.property1=value1" + NEW_LINE ); - writer.write( prefix + "test.property2=value2" + NEW_LINE ); - writer.write( prefix + "test.property3=value3" + NEW_LINE ); + String prefix = keyPrefix; + if ( prefix == null ) + { + prefix = ""; + } + for ( Map.Entry entry : properties.entrySet() ) + { + writer.write( prefix + entry.getKey() + "=" + entry.getValue() + NEW_LINE ); + } writer.flush(); } - finally - { - writer.close(); - } return f; } } diff --git a/src/test/java/org/codehaus/mojo/properties/WriteProjectPropertiesMojoTest.java b/src/test/java/org/codehaus/mojo/properties/WriteProjectPropertiesMojoTest.java new file mode 100644 index 0000000..501ee77 --- /dev/null +++ b/src/test/java/org/codehaus/mojo/properties/WriteProjectPropertiesMojoTest.java @@ -0,0 +1,141 @@ +package org.codehaus.mojo.properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.project.MavenProject; +import org.junit.Before; +import org.junit.Test; + +public class WriteProjectPropertiesMojoTest +{ + private static final Map ISO_8859_REPRESENTABLE = Map.of("test.property1", "value1®", + "test.property2", "value2"); + + private static final Map ISO_8859_NOT_REPRESENTABLE = Map.of ("test.property3", "value3™", + "test.property4", "κόσμε"); + + private MavenProject projectStub; + private WriteProjectProperties writePropertiesMojo; + + @Before + public void setUp() + { + projectStub = new MavenProject(); + writePropertiesMojo = new WriteProjectProperties(); + writePropertiesMojo.setProject( projectStub ); + } + + @Test + public void writePropertiesDefaultEncoding() throws Exception + { + final Map propMap = new HashMap<>(); + propMap.putAll( ISO_8859_REPRESENTABLE ); + propMap.putAll( ISO_8859_NOT_REPRESENTABLE ); + + final String encoding = AbstractWritePropertiesMojo.DEFAULT_ENCODING; + + File outputFile = File.createTempFile( "prop-test", ".properties" ); + outputFile.deleteOnExit(); + writePropertiesMojo.setOutputFile( outputFile ); + + Properties projectProperties = projectStub.getProperties(); + projectProperties.putAll( propMap ); + + writePropertiesMojo.execute(); + + try ( FileInputStream fileStream = new FileInputStream( outputFile ); + InputStreamReader fr = new InputStreamReader( fileStream, encoding ) ) + { + // load the properties we just saved + Properties savedProperties = new Properties(); + savedProperties.load( fr ); + + // it should not be empty + assertNotEquals( 0, savedProperties.size() ); + + // we are not adding prefix, so properties should be same as in file + assertEquals( projectProperties.size(), savedProperties.size() ); + + // strings which are representable in ISO-8859-1 should match the source data + for ( String sourceKey : ISO_8859_REPRESENTABLE.keySet() ) + { + assertEquals( ISO_8859_REPRESENTABLE.get( sourceKey ), savedProperties.getProperty( sourceKey ) ); + } + + // strings which are not representable in ISO-8859-1 underwent a conversion which lost data + for ( String sourceKey : ISO_8859_NOT_REPRESENTABLE.keySet() ) + { + String sourceValue = ISO_8859_NOT_REPRESENTABLE.get( sourceKey ); + String converted = new String( sourceValue.getBytes( encoding ), StandardCharsets.UTF_8 ); + String propVal = savedProperties.getProperty( sourceKey ); + assertNotEquals( sourceValue, propVal ); + assertEquals( converted, propVal ); + } + } + } + + @Test + public void writePropertiesUTF8() throws Exception + { + final Map propMap = new HashMap<>(); + propMap.putAll( ISO_8859_REPRESENTABLE ); + propMap.putAll( ISO_8859_NOT_REPRESENTABLE ); + + final String encoding = StandardCharsets.UTF_8.name(); + + File outputFile = File.createTempFile( "prop-test", ".properties" ); + outputFile.deleteOnExit(); + writePropertiesMojo.setOutputFile( outputFile ); + + Properties projectProperties = projectStub.getProperties(); + projectProperties.putAll( propMap ); + + writePropertiesMojo.setEncoding( encoding ); + writePropertiesMojo.execute(); + + try ( FileInputStream fileStream = new FileInputStream( outputFile ); + InputStreamReader fr = new InputStreamReader( fileStream, encoding ) ) + { + // load the properties we just saved + Properties savedProperties = new Properties(); + savedProperties.load( fr ); + + // it should not be empty + assertNotEquals( 0, savedProperties.size() ); + + // we are not adding prefix, so properties should be same as in file + assertEquals( projectProperties.size(), savedProperties.size() ); + + // all test data is representable in UTF8, all should match source data + for ( String sourceKey : propMap.keySet() ) + { + assertEquals( propMap.get( sourceKey ), savedProperties.getProperty( sourceKey ) ); + } + } + } + + @Test + public void testInvalidEncoding() throws Exception + { + final Map propMap = new HashMap<>(); + propMap.putAll(ISO_8859_REPRESENTABLE); + + File outputFile = File.createTempFile( "prop-test", ".properties" ); + outputFile.deleteOnExit(); + writePropertiesMojo.setOutputFile( outputFile ); + writePropertiesMojo.setEncoding( "invalid-encoding" ); + MojoExecutionException thrown = assertThrows( MojoExecutionException.class, () -> writePropertiesMojo.execute() ); + assertEquals( thrown.getMessage(), "Invalid encoding 'invalid-encoding'" ); + } +}