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 <rmannibu...@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(); + } +}