You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by rm...@apache.org on 2019/08/08 09:47:43 UTC
[maven-shade-plugin] branch master updated: [MSHADE-322] - adding
properties transformer (+ its microprofile and openwebbeans specific cases)
This is an automated email from the ASF dual-hosted git repository.
rmannibucau pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-shade-plugin.git
The following commit(s) were added to refs/heads/master by this push:
new f122b47 [MSHADE-322] - adding properties transformer (+ its microprofile and openwebbeans specific cases)
f122b47 is described below
commit f122b4731993ca06cf8d5a052608be4dd5fc95d3
Author: Romain Manni-Bucau <rm...@apache.org>
AuthorDate: Fri Jul 12 20:13:04 2019 +0200
[MSHADE-322] - adding properties transformer (+ its microprofile and openwebbeans specific cases)
---
.../properties/MicroprofileConfigTransformer.java | 33 +++
.../OpenWebBeansPropertiesTransformer.java | 33 +++
.../resource/properties/PropertiesTransformer.java | 234 ++++++++++++++++++++
.../properties/io/NoCloseOutputStream.java | 67 ++++++
.../io/SkipPropertiesDateLineWriter.java | 97 +++++++++
src/site/apt/examples/resource-transformers.apt.vm | 122 +++++++++++
.../properties/PropertiesTransformerTest.java | 143 +++++++++++++
.../shade/resource/rule/TransformerTesterRule.java | 235 +++++++++++++++++++++
8 files changed, 964 insertions(+)
diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/properties/MicroprofileConfigTransformer.java b/src/main/java/org/apache/maven/plugins/shade/resource/properties/MicroprofileConfigTransformer.java
new file mode 100644
index 0000000..ecc49b2
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/resource/properties/MicroprofileConfigTransformer.java
@@ -0,0 +1,33 @@
+package org.apache.maven.plugins.shade.resource.properties;
+
+/*
+ * 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.
+ */
+
+/**
+ * Enables to merge Microprofile Config configuration files properly respecting their ordinal.
+ *
+ * @since 3.2.2
+ */
+public class MicroprofileConfigTransformer extends PropertiesTransformer
+{
+ protected MicroprofileConfigTransformer()
+ {
+ super( null, "config_ordinal", 1000, false );
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/properties/OpenWebBeansPropertiesTransformer.java b/src/main/java/org/apache/maven/plugins/shade/resource/properties/OpenWebBeansPropertiesTransformer.java
new file mode 100644
index 0000000..180ac54
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/resource/properties/OpenWebBeansPropertiesTransformer.java
@@ -0,0 +1,33 @@
+package org.apache.maven.plugins.shade.resource.properties;
+
+/*
+ * 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.
+ */
+
+/**
+ * Enables to merge openwebbeans configuration files properly respecting their ordinal.
+ *
+ * @since 3.2.2
+ */
+public class OpenWebBeansPropertiesTransformer extends PropertiesTransformer
+{
+ protected OpenWebBeansPropertiesTransformer()
+ {
+ super( "META-INF/openwebbeans/openwebbeans.properties", "configuration.ordinal", 100, false );
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/properties/PropertiesTransformer.java b/src/main/java/org/apache/maven/plugins/shade/resource/properties/PropertiesTransformer.java
new file mode 100644
index 0000000..a6a7aff
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/resource/properties/PropertiesTransformer.java
@@ -0,0 +1,234 @@
+package org.apache.maven.plugins.shade.resource.properties;
+
+/*
+ * 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 java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+import org.apache.maven.plugins.shade.relocation.Relocator;
+import org.apache.maven.plugins.shade.resource.ResourceTransformer;
+import org.apache.maven.plugins.shade.resource.properties.io.NoCloseOutputStream;
+import org.apache.maven.plugins.shade.resource.properties.io.SkipPropertiesDateLineWriter;
+
+/**
+ * Enables to merge a set of properties respecting priority between them.
+ *
+ * @since 3.2.2
+ */
+public class PropertiesTransformer implements ResourceTransformer
+{
+ private String resource;
+ private String alreadyMergedKey;
+ private String ordinalKey;
+ private int defaultOrdinal;
+ private boolean reverseOrder;
+
+ private final List<Properties> properties = new ArrayList<>();
+
+ public PropertiesTransformer()
+ {
+ // no-op
+ }
+
+ protected PropertiesTransformer( final String resource, final String ordinalKey,
+ final int defaultOrdinal, final boolean reversed )
+ {
+ this.resource = resource;
+ this.ordinalKey = ordinalKey;
+ this.defaultOrdinal = defaultOrdinal;
+ this.reverseOrder = reversed;
+ }
+
+ @Override
+ public boolean canTransformResource( final String resource )
+ {
+ return Objects.equals( resource, this.resource );
+ }
+
+ @Override
+ public void processResource( final String resource, final InputStream is, final List<Relocator> relocators )
+ throws IOException
+ {
+ final Properties p = new Properties();
+ p.load( is );
+ properties.add( p );
+ }
+
+ @Override
+ public boolean hasTransformedResource()
+ {
+ return !properties.isEmpty();
+ }
+
+ @Override
+ public void modifyOutputStream( final JarOutputStream os ) throws IOException
+ {
+ if ( properties.isEmpty() )
+ {
+ return;
+ }
+
+ final Properties out = mergeProperties( sortProperties() );
+ if ( ordinalKey != null )
+ {
+ out.remove( ordinalKey );
+ }
+ if ( alreadyMergedKey != null )
+ {
+ out.remove( alreadyMergedKey );
+ }
+ os.putNextEntry( new JarEntry( resource ) );
+ final BufferedWriter writer = new SkipPropertiesDateLineWriter(
+ new OutputStreamWriter( new NoCloseOutputStream( os ), StandardCharsets.ISO_8859_1 ) );
+ out.store( writer, " Merged by maven-shade-plugin (" + getClass().getName() + ")" );
+ writer.close();
+ os.closeEntry();
+ }
+
+ public void setReverseOrder( final boolean reverseOrder )
+ {
+ this.reverseOrder = reverseOrder;
+ }
+
+ public void setResource( final String resource )
+ {
+ this.resource = resource;
+ }
+
+ public void setOrdinalKey( final String ordinalKey )
+ {
+ this.ordinalKey = ordinalKey;
+ }
+
+ public void setDefaultOrdinal( final int defaultOrdinal )
+ {
+ this.defaultOrdinal = defaultOrdinal;
+ }
+
+ public void setAlreadyMergedKey( final String alreadyMergedKey )
+ {
+ this.alreadyMergedKey = alreadyMergedKey;
+ }
+
+ private List<Properties> sortProperties()
+ {
+ final List<Properties> sortedProperties = new ArrayList<>();
+ boolean foundMaster = false;
+ for ( final Properties current : properties )
+ {
+ if ( alreadyMergedKey != null )
+ {
+ final String master = current.getProperty( alreadyMergedKey );
+ if ( Boolean.parseBoolean( master ) )
+ {
+ if ( foundMaster )
+ {
+ throw new IllegalStateException(
+ "Ambiguous merged values: " + sortedProperties + ", " + current );
+ }
+ foundMaster = true;
+ sortedProperties.clear();
+ sortedProperties.add( current );
+ }
+ }
+ if ( !foundMaster )
+ {
+ final int configOrder = getConfigurationOrdinal( current );
+
+ int i;
+ for ( i = 0; i < sortedProperties.size(); i++ )
+ {
+ int listConfigOrder = getConfigurationOrdinal( sortedProperties.get( i ) );
+ if ( ( !reverseOrder && listConfigOrder > configOrder )
+ || ( reverseOrder && listConfigOrder < configOrder ) )
+ {
+ break;
+ }
+ }
+ sortedProperties.add( i, current );
+ }
+ }
+ return sortedProperties;
+ }
+
+ private int getConfigurationOrdinal( final Properties p )
+ {
+ if ( ordinalKey == null )
+ {
+ return defaultOrdinal;
+ }
+ final String configOrderString = p.getProperty( ordinalKey );
+ if ( configOrderString != null && configOrderString.length() > 0 )
+ {
+ return Integer.parseInt( configOrderString );
+ }
+ return defaultOrdinal;
+ }
+
+ private static Properties mergeProperties( final List<Properties> sortedProperties )
+ {
+ final Properties mergedProperties = new Properties()
+ {
+ @Override
+ public synchronized Enumeration<Object> keys() // ensure it is sorted to be deterministic
+ {
+ final List<String> keys = new LinkedList<>();
+ for ( Object k : super.keySet() )
+ {
+ keys.add( (String) k );
+ }
+ Collections.sort( keys );
+ final Iterator<String> it = keys.iterator();
+ return new Enumeration<Object>()
+ {
+ @Override
+ public boolean hasMoreElements()
+ {
+ return it.hasNext();
+ }
+
+ @Override
+ public Object nextElement()
+ {
+ return it.next();
+ }
+ };
+ }
+ };
+ for ( final Properties p : sortedProperties )
+ {
+ mergedProperties.putAll( p );
+ }
+ return mergedProperties;
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/properties/io/NoCloseOutputStream.java b/src/main/java/org/apache/maven/plugins/shade/resource/properties/io/NoCloseOutputStream.java
new file mode 100644
index 0000000..b956044
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/resource/properties/io/NoCloseOutputStream.java
@@ -0,0 +1,67 @@
+package org.apache.maven.plugins.shade.resource.properties.io;
+
+/*
+ * 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 java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Simple output stream replacing close call by a simpe flush.
+ * Useful for output streams nesting streams (like jar output streams) and using a stream encoder.
+ */
+public class NoCloseOutputStream extends OutputStream
+{
+ private final OutputStream delegate;
+
+ public NoCloseOutputStream( OutputStream delegate )
+ {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void write( int b ) throws IOException
+ {
+ delegate.write( b );
+ }
+
+ @Override
+ public void write( byte[] b ) throws IOException
+ {
+ delegate.write( b );
+ }
+
+ @Override
+ public void write( byte[] b, int off, int len ) throws IOException
+ {
+ delegate.write( b, off, len );
+ }
+
+ @Override
+ public void flush() throws IOException
+ {
+ delegate.flush();
+ }
+
+ @Override
+ public void close() throws IOException
+ {
+ delegate.flush();
+ }
+}
diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/properties/io/SkipPropertiesDateLineWriter.java b/src/main/java/org/apache/maven/plugins/shade/resource/properties/io/SkipPropertiesDateLineWriter.java
new file mode 100644
index 0000000..663c700
--- /dev/null
+++ b/src/main/java/org/apache/maven/plugins/shade/resource/properties/io/SkipPropertiesDateLineWriter.java
@@ -0,0 +1,97 @@
+package org.apache.maven.plugins.shade.resource.properties.io;
+
+/*
+ * 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 java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Simple buffered writer skipping its first write(String) call.
+ */
+public class SkipPropertiesDateLineWriter extends BufferedWriter
+{
+ private State currentState = State.MUST_SKIP_DATE_COMMENT;
+
+ public SkipPropertiesDateLineWriter( Writer out )
+ {
+ super( out );
+ }
+
+ @Override
+ public void write( String str ) throws IOException
+ {
+ if ( currentState.shouldSkip( str ) )
+ {
+ currentState = currentState.next();
+ return;
+ }
+ super.write( str );
+ }
+
+ private enum State
+ {
+ MUST_SKIP_DATE_COMMENT
+ {
+ @Override
+ boolean shouldSkip( String content )
+ {
+ return content.length() > 1 && content.startsWith( "#" ) && !content.startsWith( "# " );
+ }
+
+ @Override
+ State next()
+ {
+ return SKIPPED_DATE_COMMENT;
+ }
+ },
+ SKIPPED_DATE_COMMENT
+ {
+ @Override
+ boolean shouldSkip( String content )
+ {
+ return content.trim().isEmpty();
+ }
+
+ @Override
+ State next()
+ {
+ return DONE;
+ }
+ },
+ DONE
+ {
+ @Override
+ boolean shouldSkip( String content )
+ {
+ return false;
+ }
+
+ @Override
+ State next()
+ {
+ throw new UnsupportedOperationException( "done is a terminal state" );
+ }
+ };
+
+ abstract boolean shouldSkip( String content );
+ abstract State next();
+ }
+}
diff --git a/src/site/apt/examples/resource-transformers.apt.vm b/src/site/apt/examples/resource-transformers.apt.vm
index 1a09833..7248027 100644
--- a/src/site/apt/examples/resource-transformers.apt.vm
+++ b/src/site/apt/examples/resource-transformers.apt.vm
@@ -47,8 +47,14 @@ Resource Transformers
*-----------------------------------------+------------------------------------------+
| {{ManifestResourceTransformer}} | Sets entries in the <<<MANIFEST>>> |
*-----------------------------------------+------------------------------------------+
+| {{MicroprofileConfigTransformer}} | Merges conflicting Microprofile Config properties based on an ordinal |
+*-----------------------------------------+------------------------------------------+
+| {{OpenWebBeansPropertiesTransformer}} | Merges Apache OpenWebBeans configuration files |
+*-----------------------------------------+------------------------------------------+
| {{PluginXmlResourceTransformer}} | Aggregates Mavens <<<plugin.xml>>> |
*-----------------------------------------+------------------------------------------+
+| {{PropertiesTransformer}} | Merges properties files owning an ordinal to solve conflicts |
+*-----------------------------------------+------------------------------------------+
| {{ResourceBundleAppendingTransformer}} | Merges ResourceBundles |
*-----------------------------------------+------------------------------------------+
| {{ServicesResourceTransformer}} | Relocated class names in <<<META-INF/services>>> resources and merges them. |
@@ -538,3 +544,119 @@ Transformers in <<<org.apache.maven.plugins.shade.resource>>>
...
</project>
+-----
+
+* Merging properties files with {PropertiesTransformer}
+
+ The <<<PropertiesTransformer>>> allows a set of properties files to be merged and to resolve conflicts
+ based on an ordinal giving the priority of each file.
+ An optional <<<alreadyMergedKey>>> enables to have a boolean flag in the file which, if set to true,
+ request to use the file as it as the result of the merge. If two files are considered complete in the
+ merge process then the shade will fail.
+
++-----
+<project>
+ ...
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>${project.version}</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <transformers>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.properties.PropertiesTransformer">
+ <!-- required configuration -->
+ <resource>configuration/application.properties</resource>
+ <ordinalKey>ordinal</ordinalKey>
+ <!-- optional configuration -->
+ <alreadyMergedKey>already_merged</alreadyMergedKey>
+ <defaultOrdinal>0</defaultOrdinal>
+ <reverseOrder>false</reverseOrder>
+ </transformer>
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ ...
+</project>
++-----
+
+* Merging Apache OpenWebBeans configuration with {OpenWebBeansPropertiesTransformer}
+
+ <<<OpenWebBeansPropertiesTransformer>>> preconfigure a <<<PropertiesTransformer>>>
+ for Apache OpenWebBeans configuration files.
+
+
++-----
+<project>
+ ...
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>${project.version}</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <transformers>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.properties.OpenWebBeansPropertiesTransformer" />
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ ...
+</project>
++-----
+
+* Merging Microprofile Config properties with {MicroprofileConfigTransformer}
+
+ <<<MicroprofileConfigTransformer>>> preconfigure a <<<PropertiesTransformer>>>
+ for Microprofile Config. The only required configuration is the ordinal.
+ The <<<alreadyMergedKey>>> is supported but is not defined by the specification.
+
+
++-----
+<project>
+ ...
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>${project.version}</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <transformers>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.properties.MicroprofileConfigTransformer">
+ <resource>configuration/app.properties</resource>
+ </transformer>
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ ...
+</project>
++-----
+
diff --git a/src/test/java/org/apache/maven/plugins/shade/resource/properties/PropertiesTransformerTest.java b/src/test/java/org/apache/maven/plugins/shade/resource/properties/PropertiesTransformerTest.java
new file mode 100644
index 0000000..bd5f0e1
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/shade/resource/properties/PropertiesTransformerTest.java
@@ -0,0 +1,143 @@
+package org.apache.maven.plugins.shade.resource.properties;
+
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+
+import org.apache.maven.plugins.shade.resource.properties.io.NoCloseOutputStream;
+import org.apache.maven.plugins.shade.resource.properties.io.SkipPropertiesDateLineWriter;
+import org.apache.maven.plugins.shade.resource.rule.TransformerTesterRule;
+import org.apache.maven.plugins.shade.resource.rule.TransformerTesterRule.Property;
+import org.apache.maven.plugins.shade.resource.rule.TransformerTesterRule.Resource;
+import org.apache.maven.plugins.shade.resource.rule.TransformerTesterRule.TransformerTest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+public class PropertiesTransformerTest
+{
+ @Rule
+ public final TestRule tester = new TransformerTesterRule();
+
+ @Test
+ public void propertiesRewritingIsStable() throws IOException
+ {
+ final Properties properties = new Properties();
+ properties.setProperty("a", "1");
+ properties.setProperty("b", "2");
+
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ final BufferedWriter writer = new SkipPropertiesDateLineWriter(
+ new OutputStreamWriter( new NoCloseOutputStream( os ), StandardCharsets.ISO_8859_1 ) );
+ properties.store( writer, " Merged by maven-shade-plugin" );
+ writer.close();
+ os.close();
+
+ assertEquals(
+ "# Merged by maven-shade-plugin\n" +
+ "b=2\n" +
+ "a=1\n", os.toString("UTF-8"));
+ }
+
+ @Test
+ public void canTransform()
+ {
+ final PropertiesTransformer transformer = new PropertiesTransformer();
+ transformer.setResource("foo/bar/my.properties");
+ assertTrue(transformer.canTransformResource("foo/bar/my.properties"));
+ assertFalse(transformer.canTransformResource("whatever"));
+ }
+
+ @Test
+ @TransformerTest(
+ transformer = PropertiesTransformer.class,
+ configuration = @Property(name = "resource", value = "foo/bar/my.properties"),
+ visited = {
+ @Resource(path = "foo/bar/my.properties", content = "a=b"),
+ @Resource(path = "foo/bar/my.properties", content = "c=d"),
+ },
+ expected = @Resource(path = "foo/bar/my.properties", content = "#.*\na=b\nc=d\n")
+ )
+ public void mergeWithoutOverlap()
+ {
+ }
+
+ @Test
+ @TransformerTest(
+ transformer = PropertiesTransformer.class,
+ configuration = {
+ @Property(name = "resource", value = "foo/bar/my.properties"),
+ @Property(name = "ordinalKey", value = "priority")
+ },
+ visited = {
+ @Resource(path = "foo/bar/my.properties", content = "a=d\npriority=3"),
+ @Resource(path = "foo/bar/my.properties", content = "a=b\npriority=1"),
+ @Resource(path = "foo/bar/my.properties", content = "a=c\npriority=2"),
+ },
+ expected = @Resource(path = "foo/bar/my.properties", content = "#.*\na=d\n")
+ )
+ public void mergeWithOverlap()
+ {
+ }
+
+ @Test
+ @TransformerTest(
+ transformer = PropertiesTransformer.class,
+ configuration = {
+ @Property(name = "resource", value = "foo/bar/my.properties"),
+ @Property(name = "alreadyMergedKey", value = "complete")
+ },
+ visited = {
+ @Resource(path = "foo/bar/my.properties", content = "a=b\ncomplete=true"),
+ @Resource(path = "foo/bar/my.properties", content = "a=c\npriority=2"),
+ },
+ expected = @Resource(path = "foo/bar/my.properties", content = "#.*\na=b\n")
+ )
+ public void mergeWithAlreadyMerged()
+ {
+ }
+
+ @Test
+ @TransformerTest(
+ transformer = PropertiesTransformer.class,
+ configuration = {
+ @Property(name = "resource", value = "foo/bar/my.properties"),
+ @Property(name = "alreadyMergedKey", value = "complete")
+ },
+ visited = {
+ @Resource(path = "foo/bar/my.properties", content = "a=b\ncomplete=true"),
+ @Resource(path = "foo/bar/my.properties", content = "a=c\ncomplete=true"),
+ },
+ expected = {},
+ expectedException = IllegalStateException.class
+ )
+ public void alreadyMergeConflict()
+ {
+ }
+}
diff --git a/src/test/java/org/apache/maven/plugins/shade/resource/rule/TransformerTesterRule.java b/src/test/java/org/apache/maven/plugins/shade/resource/rule/TransformerTesterRule.java
new file mode 100644
index 0000000..88d3e93
--- /dev/null
+++ b/src/test/java/org/apache/maven/plugins/shade/resource/rule/TransformerTesterRule.java
@@ -0,0 +1,235 @@
+package org.apache.maven.plugins.shade.resource.rule;
+
+/*
+ * 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 static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
+
+import org.apache.maven.plugins.shade.relocation.Relocator;
+import org.apache.maven.plugins.shade.resource.ResourceTransformer;
+import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
+import org.codehaus.plexus.component.configurator.converters.ConfigurationConverter;
+import org.codehaus.plexus.component.configurator.converters.lookup.ConverterLookup;
+import org.codehaus.plexus.component.configurator.converters.lookup.DefaultConverterLookup;
+import org.codehaus.plexus.component.configurator.expression.DefaultExpressionEvaluator;
+import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
+import org.codehaus.plexus.configuration.PlexusConfiguration;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+public class TransformerTesterRule implements TestRule
+{
+ @Override
+ public Statement apply( final Statement base, final Description description )
+ {
+ return new Statement()
+ {
+ @Override
+ public void evaluate() throws Throwable
+ {
+ final TransformerTest spec = description.getAnnotation( TransformerTest.class );
+ if ( spec == null )
+ {
+ base.evaluate();
+ return;
+ }
+
+ final Map<String, String> jar;
+ try
+ {
+ final ResourceTransformer transformer = createTransformer(spec);
+ visit(spec, transformer);
+ jar = captureOutput(transformer);
+ }
+ catch ( final Exception ex )
+ {
+ if ( Exception.class.isAssignableFrom( spec.expectedException() ) )
+ {
+ assertTrue(
+ ex.getClass().getName(),
+ spec.expectedException().isAssignableFrom( ex.getClass() ) );
+ return;
+ }
+ else
+ {
+ throw ex;
+ }
+ }
+ asserts(spec, jar);
+ }
+ };
+ }
+
+ private void asserts( final TransformerTest spec, final Map<String, String> jar)
+ {
+ if ( spec.strictMatch() && jar.size() != spec.expected().length )
+ {
+ fail( "Strict match test failed: " + jar );
+ }
+ for ( final Resource expected : spec.expected() )
+ {
+ final String content = jar.get( expected.path() );
+ assertNotNull( expected.path(), content );
+ assertTrue(
+ expected.path() + ", expected=" + expected.content() + ", actual=" + content,
+ content.matches( expected.content() ) );
+ }
+ }
+
+ private Map<String, String> captureOutput(final ResourceTransformer transformer ) throws IOException
+ {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try ( final JarOutputStream jar = new JarOutputStream( out ) )
+ {
+ transformer.modifyOutputStream( jar );
+ }
+
+ final Map<String, String> created = new HashMap<>();
+ try ( final JarInputStream jar = new JarInputStream( new ByteArrayInputStream( out.toByteArray() ) ) )
+ {
+ JarEntry entry;
+ while ( ( entry = jar.getNextJarEntry() ) != null )
+ {
+ created.put( entry.getName(), read( jar ) );
+ }
+ }
+ return created;
+ }
+
+ private void visit( final TransformerTest spec, final ResourceTransformer transformer ) throws IOException
+ {
+ for ( final Resource resource : spec.visited() )
+ {
+ if ( transformer.canTransformResource( resource.path() ))
+ {
+ transformer.processResource(
+ resource.path(),
+ new ByteArrayInputStream( resource.content().getBytes(StandardCharsets.UTF_8) ),
+ Collections.<Relocator>emptyList() );
+ }
+ }
+ }
+
+ private String read(final JarInputStream jar) throws IOException
+ {
+ final StringBuilder builder = new StringBuilder();
+ final byte[] buffer = new byte[512];
+ int read;
+ while ( (read = jar.read(buffer) ) >= 0 )
+ {
+ builder.append( new String( buffer, 0, read ) );
+ }
+ return builder.toString();
+ }
+
+ private ResourceTransformer createTransformer(final TransformerTest spec)
+ {
+ final ConverterLookup lookup = new DefaultConverterLookup();
+ try
+ {
+ final ConfigurationConverter converter = lookup.lookupConverterForType( spec.transformer() );
+ final PlexusConfiguration configuration = new DefaultPlexusConfiguration( "configuration" );
+ for ( final Property property : spec.configuration() )
+ {
+ configuration.addChild( property.name(), property.value() );
+ }
+ return ResourceTransformer.class.cast(
+ converter.fromConfiguration( lookup, configuration, spec.transformer(), spec.transformer(),
+ Thread.currentThread().getContextClassLoader(),
+ new DefaultExpressionEvaluator() ) );
+ }
+ catch (final ComponentConfigurationException e)
+ {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Enables to describe a test without having to implement the logic itself.
+ */
+ @Target(METHOD)
+ @Retention(RUNTIME)
+ public @interface TransformerTest
+ {
+ /**
+ * @return the list of resource the transformer will process.
+ */
+ Resource[] visited();
+
+ /**
+ * @return the expected output created by the transformer.
+ */
+ Resource[] expected();
+
+ /**
+ * @return true if only expected resources must be found.
+ */
+ boolean strictMatch() default true;
+
+ /**
+ * @return type of transformer to use.
+ */
+ Class<?> transformer();
+
+ /**
+ * @return transformer configuration.
+ */
+ Property[] configuration();
+
+ /**
+ * @return if set to an exception class it ensures it is thrown during the processing.
+ */
+ Class<?> expectedException() default Object.class;
+ }
+
+ @Target(METHOD)
+ @Retention(RUNTIME)
+ public @interface Property
+ {
+ String name();
+ String value();
+ }
+
+ @Target(METHOD)
+ @Retention(RUNTIME)
+ public @interface Resource
+ {
+ String path();
+ String content();
+ }
+}