diff --git a/log4j-transform-maven-shade-plugin-extensions/pom.xml b/log4j-transform-maven-shade-plugin-extensions/pom.xml new file mode 100644 index 00000000..cc30e877 --- /dev/null +++ b/log4j-transform-maven-shade-plugin-extensions/pom.xml @@ -0,0 +1,109 @@ + + + + 4.0.0 + + org.apache.logging.log4j + log4j-transform-parent + ${revision} + ../log4j-transform-parent + + log4j-transform-maven-shade-plugin-extensions + jar + Apache Log4j Maven Shade Plugin Transformer + Transformer implementation to concatenate Log4j2Plugins.dat files + + Shaded Plugin Log4j2 Transformer + true + + + + + commons-io + commons-io + + + org.apache.logging.log4j + log4j-api + + + org.apache.logging.log4j + log4j-core + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + provided + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + org.jacoco + jacoco-maven-plugin + + ${project.build.directory}/jacoco-report + + + PACKAGE + + + LINE + COVEREDRATIO + 0.96 + + + + + + + + prepare-code-coverage + + prepare-agent + + + + report-code-coverage + + report + + + ${project.reporting.outputDirectory}/jacoco-aggregate + + + + verify-test-coverage + + check + + verify + + + + + + diff --git a/log4j-transform-maven-shade-plugin-extensions/src/main/java/org/apache/logging/log4j/maven/plugins/shade/transformer/CloseShieldOutputStream.java b/log4j-transform-maven-shade-plugin-extensions/src/main/java/org/apache/logging/log4j/maven/plugins/shade/transformer/CloseShieldOutputStream.java new file mode 100644 index 00000000..d3acae03 --- /dev/null +++ b/log4j-transform-maven-shade-plugin-extensions/src/main/java/org/apache/logging/log4j/maven/plugins/shade/transformer/CloseShieldOutputStream.java @@ -0,0 +1,45 @@ +/* + * 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. + */ +package org.apache.logging.log4j.maven.plugins.shade.transformer; + + +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.io.output.ProxyOutputStream; + +import static org.apache.commons.io.output.ClosedOutputStream.CLOSED_OUTPUT_STREAM; + +/** + * Suppress the close of underlying output stream. + */ +final class CloseShieldOutputStream extends ProxyOutputStream { + + /** + * @param out the OutputStream to delegate to + */ + /* default */ CloseShieldOutputStream(final OutputStream out) { + super(out); + } + + + @Override + public void close() throws IOException { + out.flush(); + out = CLOSED_OUTPUT_STREAM; + } +} diff --git a/log4j-transform-maven-shade-plugin-extensions/src/main/java/org/apache/logging/log4j/maven/plugins/shade/transformer/Log4j2PluginCacheFileTransformer.java b/log4j-transform-maven-shade-plugin-extensions/src/main/java/org/apache/logging/log4j/maven/plugins/shade/transformer/Log4j2PluginCacheFileTransformer.java new file mode 100644 index 00000000..ddc80162 --- /dev/null +++ b/log4j-transform-maven-shade-plugin-extensions/src/main/java/org/apache/logging/log4j/maven/plugins/shade/transformer/Log4j2PluginCacheFileTransformer.java @@ -0,0 +1,211 @@ +/* + * 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. + */ +package org.apache.logging.log4j.maven.plugins.shade.transformer; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import org.apache.logging.log4j.core.config.plugins.processor.PluginCache; +import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry; + +import org.apache.maven.plugins.shade.relocation.Relocator; +import org.apache.maven.plugins.shade.resource.ReproducibleResourceTransformer; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; + +import static org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor.PLUGIN_CACHE_FILE; + +/** + * 'log4j-maven-shade-plugin' transformer implementation. + */ +public class Log4j2PluginCacheFileTransformer + implements ReproducibleResourceTransformer { + + /** + * Log4j config files to share across the transformation stages. + */ + private final List tempFiles; + /** + * {@link Relocator} instances to share across the transformation stages. + */ + private final List tempRelocators; + /** + * Store youngest (i.e. largest millisecond) so that we can produce reproducible jar file + */ + private long youngestTime = 0; + + + /** + * Default constructor, initializing internal state. + */ + public Log4j2PluginCacheFileTransformer() { + tempRelocators = new ArrayList<>(); + tempFiles = new ArrayList<>(); + } + + /** + * @param resource resource to check + * @return true when resource is recognized as log4j-plugin-cache file + */ + @Override + public boolean canTransformResource(final String resource) { + return PLUGIN_CACHE_FILE.equals(resource); + } + + @Override + @Deprecated + public void processResource(String resource, InputStream is, List relocators) { + // stub + } + + /** + * @param resource ignored parameter + * @param resourceInput resource input stream to save in temp file + * for next stage + * @param relocators relocators to keep for next stage + * @throws IOException thrown by file writing errors + */ + @Override + public void processResource(final String resource, + final InputStream resourceInput, + final List relocators, + final long time) throws IOException { + final Path tempFile = Files.createTempFile("Log4j2Plugins", "dat"); + Files.copy(resourceInput, tempFile, REPLACE_EXISTING); + tempFiles.add(tempFile); + youngestTime = Math.max(youngestTime, time); + + if (relocators != null) { + this.tempRelocators.addAll(relocators); + } + } + + /** + * @return true when several log4j-cache-files should be merged + * or at least one relocated. + */ + @Override + public boolean hasTransformedResource() { + return tempFiles.size() > 1 + || !tempFiles.isEmpty() && !tempRelocators.isEmpty(); + } + + + /** + * Stores all previously collected log4j-cache-files to the target jar. + * + * @param jos jar output + * @throws IOException When the IO blows up + */ + @Override + public void modifyOutputStream(final JarOutputStream jos) + throws IOException { + try { + final PluginCache aggregator = new PluginCache(); + aggregator.loadCacheFiles(getUrls()); + relocatePlugin(tempRelocators, aggregator.getAllCategories()); + putJarEntry(jos); + // prevent the aggregator to close the jar output + final CloseShieldOutputStream outputStream = + new CloseShieldOutputStream(jos); + aggregator.writeCache(outputStream); + } finally { + deleteTempFiles(); + } + } + + private Enumeration getUrls() throws MalformedURLException { + final List urls = new ArrayList<>(); + for (final Path tempFile : tempFiles) { + final URL url = tempFile.toUri().toURL(); + urls.add(url); + } + return Collections.enumeration(urls); + } + + /** + * Applies the given {@code relocators} to the {@code aggregator}. + * + * @param relocators relocators. + * @param aggregatorCategories all categories of the aggregator + */ + /* default */ void relocatePlugin(final List relocators, + Map> aggregatorCategories) { + for (final Entry> categoryEntry + : aggregatorCategories.entrySet()) { + for (final Entry pluginMapEntry + : categoryEntry.getValue().entrySet()) { + final PluginEntry pluginEntry = pluginMapEntry.getValue(); + final String originalClassName = pluginEntry.getClassName(); + + final Relocator matchingRelocator = findFirstMatchingRelocator( + originalClassName, relocators); + + if (matchingRelocator != null) { + final String newClassName = matchingRelocator + .relocateClass(originalClassName); + pluginEntry.setClassName(newClassName); + } + } + } + } + + private Relocator findFirstMatchingRelocator(final String originalClassName, + final List relocators) { + Relocator result = null; + for (final Relocator relocator : relocators) { + if (relocator.canRelocateClass(originalClassName)) { + result = relocator; + break; + } + } + return result; + } + + private void putJarEntry(JarOutputStream jos) throws IOException { + final JarEntry jarEntry = new JarEntry(PLUGIN_CACHE_FILE); + + // Set time to youngest timestamp, to ensure reproducible output. + final FileTime fileTime = FileTime.fromMillis(youngestTime); + jarEntry.setLastModifiedTime(fileTime); + + jos.putNextEntry(jarEntry); + } + + private void deleteTempFiles() throws IOException { + final ListIterator pathIterator = tempFiles.listIterator(); + while (pathIterator.hasNext()) { + final Path path = pathIterator.next(); + Files.deleteIfExists(path); + pathIterator.remove(); + } + } +} diff --git a/log4j-transform-maven-shade-plugin-extensions/src/site/markdown/index.md b/log4j-transform-maven-shade-plugin-extensions/src/site/markdown/index.md new file mode 100644 index 00000000..f9cba3cf --- /dev/null +++ b/log4j-transform-maven-shade-plugin-extensions/src/site/markdown/index.md @@ -0,0 +1,88 @@ + + + +# Log4j Maven Shaded Plugin Support + +This module provides support for [Apache Maven Shade Plugin](https://maven.apache.org/plugins/maven-shade-plugin/). + +## Introduction to the problem + +Log4j 2 is composed of plugins and they are loaded from Log4j2Plugins.dat file found in the classpath at runtime. +Shading overrides the cache files provided by each individual module. For instance, -web and -core, etc. ... +So if you happen to shade libraries providing Log4j 2 plugins, you need this thing. + + +## Overview + +This module includes the transformer for [Apache Maven Shade Plugin](https://maven.apache.org/plugins/maven-shade-plugin/), that concatenates `Log4j2Plugins.dat` files, +so it must be used when there are several Log4j2Plugins.dat files in the fat jar dependencies. + +For example a fat jar must be assembled with `org.apache.logging.log4j:log4j-web` that for sure requires also `org.apache.logging.log4j:log4j-core`. Still both includes `Log4j2Plugins.dat` resources the transformer must be configured. + +## Usage + +The transformer configuration must augment standard [Apache Maven Shade Plugin](https://maven.apache.org/plugins/maven-shade-plugin/) configuration in `pom.xml`. + +```xml + + + ... + + ... + + ... + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + ... + + + + ... + + + + + + org.apache.logging.maven + log4j-maven-shade-plugin-extensions + ${log4jVersion} + + + + + + + + +``` +In the above example `${log4jVersion}` placeholder should point to the same version of the fat jar dependencies of `org.apache.logging.log4j` group + +# Legacy + +Initially the transformer was developed in this repository https://github.com/edwgiz/maven-shaded-log4j-transformer diff --git a/log4j-transform-maven-shade-plugin-extensions/src/site/site.xml b/log4j-transform-maven-shade-plugin-extensions/src/site/site.xml new file mode 100644 index 00000000..55640a30 --- /dev/null +++ b/log4j-transform-maven-shade-plugin-extensions/src/site/site.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/log4j-transform-maven-shade-plugin-extensions/src/test/java/org/apache/logging/log4j/maven/plugins/shade/transformer/Log4j2PluginCacheFileTransformerTest.java b/log4j-transform-maven-shade-plugin-extensions/src/test/java/org/apache/logging/log4j/maven/plugins/shade/transformer/Log4j2PluginCacheFileTransformerTest.java new file mode 100644 index 00000000..56224eae --- /dev/null +++ b/log4j-transform-maven-shade-plugin-extensions/src/test/java/org/apache/logging/log4j/maven/plugins/shade/transformer/Log4j2PluginCacheFileTransformerTest.java @@ -0,0 +1,144 @@ +/* + * 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. + */ +package org.apache.logging.log4j.maven.plugins.shade.transformer; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Arrays; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.jar.JarOutputStream; + +import org.apache.logging.log4j.core.config.plugins.processor.PluginCache; +import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry; + +import org.apache.commons.io.IOUtils; +import org.apache.maven.plugins.shade.relocation.Relocator; +import org.apache.maven.plugins.shade.relocation.SimpleRelocator; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static java.util.Collections.enumeration; +import static java.util.Collections.singletonList; + +import static org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor.PLUGIN_CACHE_FILE; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +final class Log4j2PluginCacheFileTransformerTest { + + private static URL pluginUrl; + + @BeforeAll + public static void setUp() { + pluginUrl = Log4j2PluginCacheFileTransformerTest.class.getClassLoader().getResource(PLUGIN_CACHE_FILE); + } + + + @Test + public void testCanTransformResource() { + final Log4j2PluginCacheFileTransformer transformer = new Log4j2PluginCacheFileTransformer(); + assertFalse(transformer.canTransformResource(null)); + assertFalse(transformer.canTransformResource("")); + assertFalse(transformer.canTransformResource(".")); + assertFalse(transformer.canTransformResource("tmp.dat")); + assertFalse(transformer.canTransformResource(PLUGIN_CACHE_FILE + ".tmp")); + assertFalse(transformer.canTransformResource("tmp/" + PLUGIN_CACHE_FILE)); + assertTrue(transformer.canTransformResource(PLUGIN_CACHE_FILE)); + } + + @Test + public void test() throws Exception { + final Log4j2PluginCacheFileTransformer transformer = new Log4j2PluginCacheFileTransformer(); + long expectedYoungestResourceTime = 1605922127000L; // Sat Nov 21 2020 01:28:47 + try (InputStream log4jCacheFileInputStream = getClass().getClassLoader() + .getResourceAsStream(PLUGIN_CACHE_FILE)) { + transformer.processResource(PLUGIN_CACHE_FILE, log4jCacheFileInputStream, null, expectedYoungestResourceTime); + } + assertFalse(transformer.hasTransformedResource()); + + try (InputStream log4jCacheFileInputStream = getClass().getClassLoader() + .getResourceAsStream(PLUGIN_CACHE_FILE)) { + transformer.processResource(PLUGIN_CACHE_FILE, log4jCacheFileInputStream, null, 2000L); + } + assertTrue(transformer.hasTransformedResource()); + + assertTransformedCacheFile(transformer, expectedYoungestResourceTime, 1911442937); + } + + private void assertTransformedCacheFile( + @SuppressWarnings("SameParameterValue") Log4j2PluginCacheFileTransformer transformer, + @SuppressWarnings("SameParameterValue") long expectedTime, + @SuppressWarnings("SameParameterValue") long expectedHash) throws IOException { + final ByteArrayOutputStream jarBuff = new ByteArrayOutputStream(); + try(final JarOutputStream out = new JarOutputStream(jarBuff)) { + transformer.modifyOutputStream(out); + } + + try(JarInputStream in = new JarInputStream(new ByteArrayInputStream(jarBuff.toByteArray()))) { + for (;;) { + final JarEntry jarEntry = in.getNextJarEntry(); + if(jarEntry == null) { + fail("No expected resource in the output jar"); + } else if(jarEntry.getName().equals(PLUGIN_CACHE_FILE)) { + assertEquals(expectedTime, jarEntry.getTime()); + assertEquals(expectedHash, Arrays.hashCode(IOUtils.toByteArray(in))); + break; + } + } + } + } + + + @Test + public void testRelocation() throws IOException { + // test with matching relocator + testRelocation("org.apache.logging", "new.location.org.apache.logging", "new.location.org.apache.logging"); + + // test without matching relocator + testRelocation("com.apache.logging", "new.location.com.apache.logging", "org.apache.logging"); + } + + private void testRelocation(final String src, final String pattern, final String target) throws IOException { + final Log4j2PluginCacheFileTransformer transformer = new Log4j2PluginCacheFileTransformer(); + final Relocator log4jRelocator = new SimpleRelocator(src, pattern, null, null); + final PluginCache aggregator = new PluginCache(); + aggregator.loadCacheFiles(enumeration(singletonList(pluginUrl))); + + transformer.relocatePlugin(singletonList(log4jRelocator), aggregator.getAllCategories()); + + for (final Map pluginEntryMap : aggregator.getAllCategories().values()) { + for (final PluginEntry entry : pluginEntryMap.values()) { + assertTrue(entry.getClassName().startsWith(target)); + } + } + } + + @AfterAll + public static void tearDown() { + pluginUrl = null; + } +} diff --git a/log4j-transform-maven-shade-plugin-extensions/src/test/resources/META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat b/log4j-transform-maven-shade-plugin-extensions/src/test/resources/META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat new file mode 100644 index 00000000..fbe6359f Binary files /dev/null and b/log4j-transform-maven-shade-plugin-extensions/src/test/resources/META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat differ diff --git a/log4j-transform-parent/pom.xml b/log4j-transform-parent/pom.xml index 50d984d5..c79db55c 100644 --- a/log4j-transform-parent/pom.xml +++ b/log4j-transform-parent/pom.xml @@ -40,6 +40,7 @@ 3.23.1 3.12.0 1.2 + 2.11.0 2.3.31 5.9.1 2.19.1-SNAPSHOT @@ -50,6 +51,7 @@ 5.1.8 2.16 + 0.8.8 1.12.0 ${spotbugs.version}.0 3.0.0-M7 @@ -82,6 +84,11 @@ assertj-core ${assertj.version} + + commons-io + commons-io + ${commons-io.version} + org.apache.commons commons-lang3 @@ -122,6 +129,11 @@ + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + org.apache.felix maven-bundle-plugin @@ -146,6 +158,7 @@ **/target/** .java-version + src/main/resources/META-INF/MANIFEST.MF diff --git a/pom.xml b/pom.xml index b1cb6e84..056b50a6 100644 --- a/pom.xml +++ b/pom.xml @@ -134,6 +134,7 @@ log4j-transform-parent log4j-transform-maven-plugin log4j-weaver + log4j-transform-maven-shade-plugin-extensions scm:git:git@github.com:apache/logging-log4j-transform.git