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