diff --git a/src/it/projects/MSHADE-406/app/pom.xml b/src/it/projects/MSHADE-406/app/pom.xml
new file mode 100644
index 000000000..c4aafb13d
--- /dev/null
+++ b/src/it/projects/MSHADE-406/app/pom.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+ 4.0.0
+
+
+ org.apache.maven.its.shade.mshade406
+ mshade-406-parent
+ 1.0
+
+
+ app
+
+
+
+ org.apache.maven.its.shade.mshade406
+ dependency
+ ${project.version}
+
+
+
+
+
+
+ maven-compiler-plugin
+ @version.maven-compiler-plugin@
+
+ 8
+
+
+
+ maven-shade-plugin
+ @project.version@
+
+
+ package
+
+ shade
+
+
+ true
+
+
+ reproducer.mr
+ reproducer.shaded.mr
+
+
+
+
+ reproducer.app.Main
+
+
+
+
+
+
+
+
+
diff --git a/src/it/projects/MSHADE-406/app/src/main/java/reproducer/app/Main.java b/src/it/projects/MSHADE-406/app/src/main/java/reproducer/app/Main.java
new file mode 100644
index 000000000..790667b07
--- /dev/null
+++ b/src/it/projects/MSHADE-406/app/src/main/java/reproducer/app/Main.java
@@ -0,0 +1,37 @@
+/*
+ * 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 reproducer.app;
+
+import java.io.InputStream;
+import java.util.Properties;
+
+import reproducer.mr.Versioned;
+
+public final class Main {
+ public static void main(String[] args) throws Exception {
+ try (InputStream input = Versioned.class.getResourceAsStream("config.properties")) {
+ if (input == null) {
+ throw new IllegalStateException("Missing relocated resource");
+ }
+ Properties properties = new Properties();
+ properties.load(input);
+ System.out.println(Versioned.value() + ":" + properties.getProperty("value"));
+ }
+ }
+}
diff --git a/src/it/projects/MSHADE-406/dependency/pom.xml b/src/it/projects/MSHADE-406/dependency/pom.xml
new file mode 100644
index 000000000..bcfccce2b
--- /dev/null
+++ b/src/it/projects/MSHADE-406/dependency/pom.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+ 4.0.0
+
+
+ org.apache.maven.its.shade.mshade406
+ mshade-406-parent
+ 1.0
+
+
+ dependency
+
+
+
+
+ src/main/resources
+
+
+ src/main/resources11
+ META-INF/versions/11
+
+
+
+
+ maven-compiler-plugin
+ @version.maven-compiler-plugin@
+
+
+ default-compile
+
+ 8
+
+
+
+ compile-java11
+ compile
+
+ compile
+
+
+ 11
+
+ ${project.basedir}/src/main/java11
+
+ ${project.build.outputDirectory}/META-INF/versions/11
+
+
+
+
+
+ maven-jar-plugin
+ @version.maven-jar-plugin@
+
+
+
+ true
+
+
+
+
+
+
+
diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Unused.java b/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Unused.java
new file mode 100644
index 000000000..cee73a43b
--- /dev/null
+++ b/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Unused.java
@@ -0,0 +1,21 @@
+/*
+ * 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 reproducer.mr;
+
+public final class Unused {}
diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Versioned.java b/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Versioned.java
new file mode 100644
index 000000000..0641ed97a
--- /dev/null
+++ b/src/it/projects/MSHADE-406/dependency/src/main/java/reproducer/mr/Versioned.java
@@ -0,0 +1,25 @@
+/*
+ * 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 reproducer.mr;
+
+public final class Versioned {
+ public static String value() {
+ return "base";
+ }
+}
diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java11/module-info.java b/src/it/projects/MSHADE-406/dependency/src/main/java11/module-info.java
new file mode 100644
index 000000000..58568179f
--- /dev/null
+++ b/src/it/projects/MSHADE-406/dependency/src/main/java11/module-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+module reproducer.mr {
+ exports reproducer.mr;
+}
diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Unused.java b/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Unused.java
new file mode 100644
index 000000000..cee73a43b
--- /dev/null
+++ b/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Unused.java
@@ -0,0 +1,21 @@
+/*
+ * 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 reproducer.mr;
+
+public final class Unused {}
diff --git a/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Versioned.java b/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Versioned.java
new file mode 100644
index 000000000..3e9b7c087
--- /dev/null
+++ b/src/it/projects/MSHADE-406/dependency/src/main/java11/reproducer/mr/Versioned.java
@@ -0,0 +1,25 @@
+/*
+ * 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 reproducer.mr;
+
+public final class Versioned {
+ public static String value() {
+ return "java11";
+ }
+}
diff --git a/src/it/projects/MSHADE-406/dependency/src/main/resources/reproducer/mr/config.properties b/src/it/projects/MSHADE-406/dependency/src/main/resources/reproducer/mr/config.properties
new file mode 100644
index 000000000..59549b0cb
--- /dev/null
+++ b/src/it/projects/MSHADE-406/dependency/src/main/resources/reproducer/mr/config.properties
@@ -0,0 +1,18 @@
+# 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.
+
+value=root-resource
diff --git a/src/it/projects/MSHADE-406/dependency/src/main/resources11/reproducer/mr/config.properties b/src/it/projects/MSHADE-406/dependency/src/main/resources11/reproducer/mr/config.properties
new file mode 100644
index 000000000..7f01d67ba
--- /dev/null
+++ b/src/it/projects/MSHADE-406/dependency/src/main/resources11/reproducer/mr/config.properties
@@ -0,0 +1,18 @@
+# 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.
+
+value=java11-resource
diff --git a/src/it/projects/MSHADE-406/invoker.properties b/src/it/projects/MSHADE-406/invoker.properties
new file mode 100644
index 000000000..4b6d43be5
--- /dev/null
+++ b/src/it/projects/MSHADE-406/invoker.properties
@@ -0,0 +1,18 @@
+# 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.
+
+invoker.java.version = 17+
diff --git a/src/it/projects/MSHADE-406/pom.xml b/src/it/projects/MSHADE-406/pom.xml
new file mode 100644
index 000000000..acf745e44
--- /dev/null
+++ b/src/it/projects/MSHADE-406/pom.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.shade.mshade406
+ mshade-406-parent
+ 1.0
+ pom
+
+
+ dependency
+ app
+
+
+
+ UTF-8
+
+
diff --git a/src/it/projects/MSHADE-406/verify.groovy b/src/it/projects/MSHADE-406/verify.groovy
new file mode 100644
index 000000000..8f774263d
--- /dev/null
+++ b/src/it/projects/MSHADE-406/verify.groovy
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+def shadedJar = new File( basedir, "app/target/app-1.0.jar" )
+assert shadedJar.isFile()
+
+def jar = new java.util.jar.JarFile( shadedJar )
+try
+{
+ assert jar.manifest.mainAttributes.getValue( "Multi-Release" ) == "true"
+ assert jar.getJarEntry( "reproducer/shaded/mr/Versioned.class" ) != null
+ assert jar.getJarEntry(
+ "META-INF/versions/11/reproducer/shaded/mr/Versioned.class" ) != null
+ assert jar.getJarEntry( "reproducer/shaded/mr/config.properties" ) != null
+ assert jar.getJarEntry(
+ "META-INF/versions/11/reproducer/shaded/mr/config.properties" ) != null
+
+ assert jar.getJarEntry( "reproducer/mr/Versioned.class" ) == null
+ assert jar.getJarEntry( "META-INF/versions/11/reproducer/mr/Versioned.class" ) == null
+ assert jar.getJarEntry( "reproducer/mr/config.properties" ) == null
+ assert jar.getJarEntry(
+ "META-INF/versions/11/reproducer/mr/config.properties" ) == null
+
+ assert jar.getJarEntry( "reproducer/shaded/mr/Unused.class" ) == null
+ assert jar.getJarEntry(
+ "META-INF/versions/11/reproducer/shaded/mr/Unused.class" ) == null
+ assert jar.getJarEntry( "META-INF/versions/11/module-info.class" ) == null
+}
+finally
+{
+ jar.close()
+}
+
+def java = new File( System.getProperty( "java.home" ), "bin/java" )
+def process = new ProcessBuilder( java.absolutePath, "-jar", shadedJar.absolutePath )
+ .redirectErrorStream( true )
+ .start()
+def output = process.inputStream.getText( "UTF-8" )
+assert process.waitFor() == 0 : output
+assert output.trim() == "java11:java11-resource"
diff --git a/src/it/projects/MSHADE-460/invoker.properties b/src/it/projects/MSHADE-460/invoker.properties
new file mode 100644
index 000000000..4b6d43be5
--- /dev/null
+++ b/src/it/projects/MSHADE-460/invoker.properties
@@ -0,0 +1,18 @@
+# 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.
+
+invoker.java.version = 17+
diff --git a/src/it/projects/MSHADE-460/pom.xml b/src/it/projects/MSHADE-460/pom.xml
new file mode 100644
index 000000000..890196487
--- /dev/null
+++ b/src/it/projects/MSHADE-460/pom.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.shade.mshade460
+ mshade-460
+ 1.0
+
+
+ 17
+ UTF-8
+
+
+
+
+ jakarta.json.bind
+ jakarta.json.bind-api
+ 3.0.0
+
+
+ org.eclipse
+ yasson
+ 3.0.3
+
+
+
+
+
+
+ maven-compiler-plugin
+ @version.maven-compiler-plugin@
+
+
+ maven-shade-plugin
+ @project.version@
+
+
+ package
+
+ shade
+
+
+
+
+ org.apache.maven.its.shade.mshade460.Main
+
+
+
+
+
+
+
+
+
+
diff --git a/src/it/projects/MSHADE-460/src/main/java/org/apache/maven/its/shade/mshade460/Main.java b/src/it/projects/MSHADE-460/src/main/java/org/apache/maven/its/shade/mshade460/Main.java
new file mode 100644
index 000000000..7f74a4402
--- /dev/null
+++ b/src/it/projects/MSHADE-460/src/main/java/org/apache/maven/its/shade/mshade460/Main.java
@@ -0,0 +1,33 @@
+/*
+ * 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.maven.its.shade.mshade460;
+
+import java.time.Instant;
+
+import jakarta.json.bind.JsonbBuilder;
+
+public final class Main {
+ public record TimeInfo(Instant datetime) {}
+
+ public static void main(String[] args) {
+ TimeInfo value = JsonbBuilder.create()
+ .fromJson("{\"datetime\":\"2023-10-29T12:00:00Z\"}", TimeInfo.class);
+ System.out.println(value);
+ }
+}
diff --git a/src/it/projects/MSHADE-460/verify.groovy b/src/it/projects/MSHADE-460/verify.groovy
new file mode 100644
index 000000000..22ca7b4ad
--- /dev/null
+++ b/src/it/projects/MSHADE-460/verify.groovy
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+def shadedJar = new File( basedir, "target/mshade-460-1.0.jar" )
+assert shadedJar.isFile()
+
+def jar = new java.util.jar.JarFile( shadedJar )
+try
+{
+ assert jar.manifest.mainAttributes.getValue( "Multi-Release" ) == "true"
+ assert jar.getJarEntry(
+ "META-INF/versions/16/org/eclipse/yasson/internal/ClassMultiReleaseExtension.class" ) != null
+}
+finally
+{
+ jar.close()
+}
+
+def java = new File( System.getProperty( "java.home" ), "bin/java" )
+def process = new ProcessBuilder( java.absolutePath, "-jar", shadedJar.absolutePath )
+ .redirectErrorStream( true )
+ .start()
+def output = process.inputStream.getText( "UTF-8" )
+assert process.waitFor() == 0 : output
+assert output.trim() == "TimeInfo[datetime=2023-10-29T12:00:00Z]"
diff --git a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
index d4f99674d..53c65413a 100644
--- a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
+++ b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java
@@ -52,6 +52,7 @@
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
@@ -82,6 +83,12 @@
public class DefaultShader implements Shader {
private static final int BUFFER_SIZE = 32 * 1024;
+ private static final Pattern MULTI_RELEASE_ENTRY_PATH =
+ Pattern.compile("^(META-INF/versions/(?:9|[1-9][0-9]+)/)(.+)$");
+
+ private static final Pattern VERSIONED_MODULE_INFO =
+ Pattern.compile("^META-INF/versions/(?:9|[1-9][0-9]+)/module-info[.]class$");
+
private final Logger logger;
public DefaultShader() {
@@ -143,6 +150,9 @@ public void shade(ShadeRequest shadeRequest) throws IOException, MojoExecutionEx
it.remove();
}
}
+ if (manifestTransformer == null && containsMultiReleaseJar(shadeRequest.getJars())) {
+ manifestTransformer = new ManifestResourceTransformer();
+ }
final DefaultPackageMapper packageMapper = new DefaultPackageMapper(shadeRequest.getRelocators());
@@ -393,7 +403,8 @@ private boolean isExcludedEntry(final String name) {
return true;
}
- if ("module-info.class".equals(name)) {
+ if ("module-info.class".equals(name)
+ || VERSIONED_MODULE_INFO.matcher(name).matches()) {
logger.warn("Discovered module-info.class. " + "Shading will break its strong encapsulation.");
return true;
}
@@ -415,7 +426,7 @@ private void shadeJarEntry(
int method)
throws Exception {
try (InputStream in = inputProvider.call()) {
- String mappedName = packageMapper.map(name, true, false);
+ String mappedName = ArchiveEntry.parse(name).map(packageMapper);
int idx = mappedName.lastIndexOf('/');
if (idx != -1) {
@@ -460,19 +471,20 @@ private void goThroughAllJarEntriesForManifestTransformer(
throws IOException {
if (manifestTransformer != null) {
for (File jar : shadeRequest.getJars()) {
- try (JarFile jarFile = newJarFile(jar)) {
- for (Enumeration en = jarFile.entries(); en.hasMoreElements(); ) {
- JarEntry entry = en.nextElement();
- String resource = entry.getName();
- if (manifestTransformer.canTransformResource(resource)) {
- resources.add(resource);
- try (InputStream inputStream = jarFile.getInputStream(entry)) {
- manifestTransformer.processResource(
- resource, inputStream, shadeRequest.getRelocators(), getTime(entry));
- }
- break;
+ if (jar.isDirectory()) {
+ File manifestFile = new File(jar, JarFile.MANIFEST_NAME);
+ if (manifestFile.isFile()) {
+ resources.add(JarFile.MANIFEST_NAME);
+ try (InputStream inputStream = Files.newInputStream(manifestFile.toPath())) {
+ manifestTransformer.processResource(
+ JarFile.MANIFEST_NAME,
+ inputStream,
+ shadeRequest.getRelocators(),
+ manifestFile.lastModified());
}
}
+ } else {
+ processManifest(jar, shadeRequest, resources, manifestTransformer);
}
}
if (manifestTransformer.hasTransformedResource()) {
@@ -481,6 +493,49 @@ private void goThroughAllJarEntriesForManifestTransformer(
}
}
+ private void processManifest(
+ File jar, ShadeRequest shadeRequest, Set resources, ManifestResourceTransformer manifestTransformer)
+ throws IOException {
+ try (JarFile jarFile = newJarFile(jar)) {
+ for (Enumeration en = jarFile.entries(); en.hasMoreElements(); ) {
+ JarEntry entry = en.nextElement();
+ String resource = entry.getName();
+ if (manifestTransformer.canTransformResource(resource)) {
+ resources.add(resource);
+ try (InputStream inputStream = jarFile.getInputStream(entry)) {
+ manifestTransformer.processResource(
+ resource, inputStream, shadeRequest.getRelocators(), getTime(entry));
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private boolean containsMultiReleaseJar(Collection jars) throws IOException {
+ for (File jar : jars) {
+ Manifest manifest;
+ if (jar.isDirectory()) {
+ File manifestFile = new File(jar, JarFile.MANIFEST_NAME);
+ if (!manifestFile.isFile()) {
+ continue;
+ }
+ try (InputStream inputStream = Files.newInputStream(manifestFile.toPath())) {
+ manifest = new Manifest(inputStream);
+ }
+ } else {
+ try (JarFile jarFile = newJarFile(jar)) {
+ manifest = jarFile.getManifest();
+ }
+ }
+ if (manifest != null
+ && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private void showOverlappingWarning() {
logger.warn("maven-shade-plugin has detected that some files are");
logger.warn("present in two or more JARs. When this happens, only one");
@@ -619,7 +674,8 @@ private void addRemappedClass(
// that use the constant pool to determine the dependencies of a class.
ClassWriter cw = new ClassWriter(0);
- final String pkg = name.substring(0, name.lastIndexOf('/') + 1);
+ ArchiveEntry classEntry = ArchiveEntry.parse(name);
+ final String pkg = classEntry.logicalName.substring(0, classEntry.logicalName.lastIndexOf('/') + 1);
final ShadeClassRemapper cv = new ShadeClassRemapper(cw, pkg, packageMapper);
try {
@@ -638,12 +694,10 @@ private void addRemappedClass(
renamedClass = originalClass;
}
- // Need to take the .class off for remapping evaluation
- String mappedName = packageMapper.map(name.substring(0, name.indexOf('.')), true, false);
+ String mappedName = classEntry.mapClass(packageMapper);
try {
- // Now we put it back on so the class file is written out with the right extension.
- JarEntry entry = new JarEntry(mappedName + ".class");
+ JarEntry entry = new JarEntry(mappedName);
entry.setTime(time);
jos.putNextEntry(entry);
@@ -653,6 +707,33 @@ private void addRemappedClass(
}
}
+ private static final class ArchiveEntry {
+ private final String versionPrefix;
+
+ private final String logicalName;
+
+ private ArchiveEntry(String versionPrefix, String logicalName) {
+ this.versionPrefix = versionPrefix;
+ this.logicalName = logicalName;
+ }
+
+ private static ArchiveEntry parse(String name) {
+ Matcher matcher = MULTI_RELEASE_ENTRY_PATH.matcher(name);
+ return matcher.matches()
+ ? new ArchiveEntry(matcher.group(1), matcher.group(2))
+ : new ArchiveEntry("", name);
+ }
+
+ private String map(PackageMapper packageMapper) {
+ return versionPrefix + packageMapper.map(logicalName, true, false);
+ }
+
+ private String mapClass(PackageMapper packageMapper) {
+ String name = logicalName.substring(0, logicalName.length() - ".class".length());
+ return versionPrefix + packageMapper.map(name, true, false) + ".class";
+ }
+ }
+
private boolean isFiltered(List filters, String name) {
for (Filter filter : filters) {
if (filter.isFiltered(name)) {
diff --git a/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java b/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java
index c23e5c20f..db839732d 100644
--- a/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java
+++ b/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java
@@ -327,11 +327,15 @@ public boolean canFilter(File jar) {
@Override
public boolean isFiltered(String classFile) {
- String className = classFile.replace('/', '.').replaceFirst("\\.class$", "");
- Clazz clazz = new Clazz(className);
+ Clazz.ParsedFileName parsedFileName = Clazz.parseClassFileName(classFile);
+ if (parsedFileName == null) {
+ classesKept += 1;
+ return false;
+ }
+ Clazz clazz = new Clazz(parsedFileName.className);
if (removable != null && removable.contains(clazz)) {
- log.debug("Removing " + className);
+ log.debug("Removing " + parsedFileName.className);
classesRemoved += 1;
return true;
}
diff --git a/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java b/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java
index b9d63b37d..8cc23995f 100644
--- a/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java
+++ b/src/main/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformer.java
@@ -30,6 +30,8 @@
import java.util.jar.Manifest;
import org.apache.maven.plugins.shade.relocation.Relocator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* A resource processor that allows the arbitrary addition of attributes to
@@ -40,6 +42,10 @@
* @since 1.2
*/
public class ManifestResourceTransformer extends AbstractCompatibilityTransformer {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ManifestResourceTransformer.class);
+
+ private static final Attributes.Name MULTI_RELEASE = new Attributes.Name("Multi-Release");
+
private final List defaultAttributes =
Arrays.asList("Export-Package", "Import-Package", "Provide-Capability", "Require-Capability");
@@ -55,6 +61,8 @@ public class ManifestResourceTransformer extends AbstractCompatibilityTransforme
private Manifest manifest;
+ private boolean multiRelease;
+
private long time = Long.MIN_VALUE;
private String shade;
@@ -79,11 +87,15 @@ public boolean canTransformResource(String resource) {
@Override
public void processResource(String resource, InputStream is, List relocators, long time)
throws IOException {
+ Manifest incomingManifest = new Manifest(is);
+ multiRelease |=
+ Boolean.parseBoolean(incomingManifest.getMainAttributes().getValue(MULTI_RELEASE));
+
// We just want to take the first manifest we come across as that's our project's manifest. This is the behavior
// now which is situational at best. Right now there is no context passed in with the processing so we cannot
// tell what artifact is being processed.
if (!manifestDiscovered) {
- manifest = new Manifest(is);
+ manifest = incomingManifest;
if (relocators != null && !relocators.isEmpty()) {
final Attributes attributes = manifest.getMainAttributes();
@@ -129,6 +141,10 @@ public void modifyOutputStream(JarOutputStream jos) throws IOException {
Attributes attributes = manifest.getMainAttributes();
+ if (multiRelease && attributes.getValue(MULTI_RELEASE) == null) {
+ attributes.put(MULTI_RELEASE, Boolean.TRUE.toString());
+ }
+
if (mainClass != null) {
attributes.put(Attributes.Name.MAIN_CLASS, mainClass);
}
@@ -143,6 +159,11 @@ public void modifyOutputStream(JarOutputStream jos) throws IOException {
}
}
+ if (multiRelease && !Boolean.parseBoolean(attributes.getValue(MULTI_RELEASE))) {
+ LOGGER.warn("Shaded inputs contain a multi-release JAR, but the shaded JAR is explicitly configured "
+ + "with Multi-Release: false. Versioned classes will not be active.");
+ }
+
JarEntry jarEntry = new JarEntry(JarFile.MANIFEST_NAME);
jarEntry.setTime(time);
jos.putNextEntry(jarEntry);
diff --git a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java
index ed14ddf20..2fdd283da 100644
--- a/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java
+++ b/src/test/java/org/apache/maven/plugins/shade/DefaultShaderTest.java
@@ -45,6 +45,7 @@
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
@@ -152,6 +153,32 @@ public void testNoopWhenNotRelocated() throws IOException, MojoExecutionExceptio
assertEquals(3 /* 1 | 2 */, result);
}
+ @Test
+ public void testMultiReleaseManifestIsPropagatedWithoutTransformer() throws Exception {
+ TemporaryFolder temporaryFolder = new TemporaryFolder();
+ try {
+ temporaryFolder.create();
+ File projectJar = createJar(temporaryFolder.newFile("project.jar"), false);
+ File dependencyJar = createJar(temporaryFolder.newFile("dependency.jar"), true);
+ File shadedJar = temporaryFolder.newFile("shaded.jar");
+
+ ShadeRequest shadeRequest = new ShadeRequest();
+ shadeRequest.setJars(new LinkedHashSet<>(Arrays.asList(projectJar, dependencyJar)));
+ shadeRequest.setRelocators(Collections.emptyList());
+ shadeRequest.setResourceTransformers(Collections.emptyList());
+ shadeRequest.setFilters(Collections.emptyList());
+ shadeRequest.setUberJar(shadedJar);
+
+ newShader().shade(shadeRequest);
+
+ try (JarFile jarFile = new JarFile(shadedJar)) {
+ assertEquals("true", jarFile.getManifest().getMainAttributes().getValue("Multi-Release"));
+ }
+ } finally {
+ temporaryFolder.delete();
+ }
+ }
+
@Test
public void testOverlappingResourcesAreLogged() throws IOException, MojoExecutionException {
DefaultShader shader = newShader();
@@ -566,6 +593,19 @@ private void writeEntryWithoutCompression(String entryName, byte[] entryBytes, J
jos.closeEntry();
}
+ private File createJar(File file, boolean multiRelease) throws IOException {
+ Manifest manifest = new Manifest();
+ manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
+ if (multiRelease) {
+ manifest.getMainAttributes().putValue("Multi-Release", "true");
+ }
+ try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(file.toPath()), manifest)) {
+ jos.putNextEntry(new JarEntry((multiRelease ? "dependency" : "project") + ".txt"));
+ jos.closeEntry();
+ }
+ return file;
+ }
+
private void shaderWithPattern(String shadedPattern, File jar, String[] excludes) throws Exception {
Set set = new LinkedHashSet<>();
set.add(new File("src/test/jars/test-project-1.0-SNAPSHOT.jar"));
diff --git a/src/test/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformerTest.java b/src/test/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformerTest.java
index 76a1ae752..0c86c1875 100644
--- a/src/test/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformerTest.java
+++ b/src/test/java/org/apache/maven/plugins/shade/resource/ManifestResourceTransformerTest.java
@@ -168,14 +168,69 @@ public void rewriteAdditionalAttributes() throws Exception {
}
}
+ @Test
+ public void preserveMultiReleaseFromSubsequentManifest() throws Exception {
+ processManifest(createTestManifest(), Collections.emptyList());
+
+ Manifest dependencyManifest = new Manifest();
+ dependencyManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ dependencyManifest.getMainAttributes().putValue("Multi-Release", "true");
+ processManifest(dependencyManifest, Collections.emptyList());
+
+ try (JarInputStream jis =
+ new JarInputStream(new ByteArrayInputStream(writeOutput().toByteArray()))) {
+ assertEquals("true", jis.getManifest().getMainAttributes().getValue("Multi-Release"));
+ }
+ }
+
+ @Test
+ public void preserveExplicitMultiReleaseFalse() throws Exception {
+ Manifest projectManifest = createTestManifest();
+ projectManifest.getMainAttributes().putValue("Multi-Release", "false");
+ processManifest(projectManifest, Collections.emptyList());
+
+ Manifest dependencyManifest = new Manifest();
+ dependencyManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ dependencyManifest.getMainAttributes().putValue("Multi-Release", "true");
+ processManifest(dependencyManifest, Collections.emptyList());
+
+ try (JarInputStream jis =
+ new JarInputStream(new ByteArrayInputStream(writeOutput().toByteArray()))) {
+ assertEquals("false", jis.getManifest().getMainAttributes().getValue("Multi-Release"));
+ }
+ }
+
+ @Test
+ public void preserveConfiguredMultiReleaseFalse() throws Exception {
+ processManifest(createTestManifest(), Collections.emptyList());
+
+ Manifest dependencyManifest = new Manifest();
+ dependencyManifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ dependencyManifest.getMainAttributes().putValue("Multi-Release", "true");
+ processManifest(dependencyManifest, Collections.emptyList());
+ transformer.setManifestEntries(Collections.singletonMap("Multi-Release", "false"));
+
+ try (JarInputStream jis =
+ new JarInputStream(new ByteArrayInputStream(writeOutput().toByteArray()))) {
+ assertEquals("false", jis.getManifest().getMainAttributes().getValue("Multi-Release"));
+ }
+ }
+
private ByteArrayOutputStream transform(final Manifest manifest, List relocators) throws IOException {
+ processManifest(manifest, relocators);
+ return writeOutput();
+ }
+
+ private void processManifest(final Manifest manifest, List relocators) throws IOException {
final ByteArrayOutputStream mboas = new ByteArrayOutputStream();
try (OutputStream mos = mboas) {
manifest.write(mos);
}
transformer.processResource(
JarFile.MANIFEST_NAME, new ByteArrayInputStream(mboas.toByteArray()), relocators, 0);
+ }
+ private ByteArrayOutputStream writeOutput() throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
try (JarOutputStream jarOutputStream = new JarOutputStream(out)) {
transformer.modifyOutputStream(jarOutputStream);