Repository: quarkusio/quarkus
Issue: io.netty.util.Version#identify not working in uber-jars

## Describe the bug
When packaging a Quarkus application as an [uber-jar](https://quarkus.io/guides/maven-tooling#uber-jar-maven) the [io.netty.util.Version#identify](https://netty.io/4.1/api/io/netty/util/Version.html#identify--) method returns an empty map instead of the versions of the included netty artifacts. The reason is that each Netty artifact contains a `META-INF/io.netty.versions` which is not included in the uber-jar.

`io.netty.util.Version#identify` not reporting the actual Netty version for example prevents the app from being monitored via Dynatrace.

### Expected behavior
The fat-jar includes a `META-INF/io.netty.versions` file which contains the merged content of all `META-INF/io.netty.versions` of the included netty artifacts to ensure the correct behaviour of the method [io.netty.util.Version#identify](https://netty.io/4.1/api/io/netty/util/Version.html#identify--).

### Actual behavior
When programmatically executing [io.netty.util.Version#identify](https://netty.io/4.1/api/io/netty/util/Version.html#identify--) an empty map is returned instead of the version.

We also see the following Dynatrace error in the logs:
```
2021-04-12 14:13:41.565 UTC [57bf881a] warning [java  ] [agent     ] Failed to detect Netty version
2021-04-12 14:13:41.649 UTC [57bf881a] warning [java  ] [netty     ] Caught exception in IntrospectionBase
 java.lang.NullPointerException
 	at com.compuware.apm.agent.introspection.netty.NettyIntrospection$1.run(NettyIntrospection.java:167)
 	at com.compuware.apm.agent.introspection.IntrospectionBase.introspect(IntrospectionBase.java:300)
 	at com.compuware.apm.agent.introspection.IntrospectionBase.introspect(IntrospectionBase.java:213)
 	at com.compuware.apm.agent.introspection.netty.NettyIntrospection.HttpRequestReceivedEnter(NettyIntrospection.java:158)
 	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java)
 	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
 	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
 	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
 	at io.vertx.core.http.impl.Http1xOrH2CHandler.end(Http1xOrH2CHandler.java:61)
 	at io.vertx.core.http.impl.Http1xOrH2CHandler.channelRead(Http1xOrH2CHandler.java:38)
 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
 	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
 	at io.netty.handler.timeout.IdleStateHandler.channelRead(IdleStateHandler.java:286)
 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
 	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
 	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
 	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
 	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
 	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
 	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
 	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
 	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
 	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
 	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
 	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
 	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
 	at java.base/java.lang.Thread.run(Unknown Source)
```

## To Reproduce

Steps to reproduce the behavior:
1. download and unzip this sample application [netty-version-uberjar.zip](https://github.com/quarkusio/quarkus/files/6346243/netty-version-uberjar.zip)
2. run `mvn compile quarkus:dev` which will print something like:
```
__  ____  __  _____   ___  __ ____  ______
 --/ __ \\/ / / / _ | / _ \\/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\\ \\
--\\___\\_\\____/_/ |_/_/|_/_/|_|\\____/___/
2021-04-20 21:27:41,146 INFO  [io.quarkus] (Quarkus Main Thread) netty-version-uberjar 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.13.2.Final) started in 4.023s. Listening on: http://localhost:8080
2021-04-20 21:27:41,166 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-04-20 21:27:41,168 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi]
************************************
Netty Versions: {netty-buffer=netty-buffer-4.1.49.Final.d0ec961, netty-codec=netty-codec-4.1.49.Final.d0ec961, netty-codec-dns=netty-codec-dns-4.1.49.Final.d0ec961, netty-codec-http=netty-codec-http-4.1.49.Final.d0ec961, netty-codec-http2=netty-codec-http2-4.1.49.Final.d0ec961, netty-codec-socks=netty-codec-socks-4.1.49.Final.d0ec961, netty-common=netty-common-4.1.49.Final.d0ec961, netty-handler=netty-handler-4.1.49.Final.d0ec961, netty-handler-proxy=netty-handler-proxy-4.1.49.Final.d0ec961, netty-resolver=netty-resolver-4.1.49.Final.d0ec961, netty-resolver-dns=netty-resolver-dns-4.1.49.Final.d0ec961, netty-transport=netty-transport-4.1.49.Final.d0ec961}
************************************
```
3. create the uberjar using `mvn package`
4. run `java -jar target\\netty-version-uberjar-1.0.0-SNAPSHOT-runner.jar` which will print something like:
```
__  ____  __  _____   ___  __ ____  ______
 --/ __ \\/ / / / _ | / _ \\/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\\ \\
--\\___\\_\\____/_/ |_/_/|_/_/|_|\\____/___/
2021-04-20 21:29:04,225 INFO  [io.quarkus] (main) netty-version-uberjar 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.13.2.Final) started in 2.060s. Listening on: http://0.0.0.0:8080
2021-04-20 21:29:04,257 INFO  [io.quarkus] (main) Profile prod activated.
2021-04-20 21:29:04,259 INFO  [io.quarkus] (main) Installed features: [cdi]
************************************
Netty Versions: {}
************************************
2021-04-20 21:29:04,298 INFO  [io.quarkus] (main) netty-version-uberjar stopped in 0.033s
```

## Environment (please complete the following information):

### Output of `uname -a` or `ver`
Linux app-31-rq52p 3.10.0-1160.6.1.el7.x86_64 #1 SMP Wed Oct 21 13:44:38 EDT 2020 x86_64 GNU/Linux

### Output of `java -version`
openjdk version "11.0.10" 2021-01-19 LTS

### Quarkus version or git rev
Quarkus 1.13.2.Final

### Build tool (ie. output of `mvnw --version` or `gradlew --version`)
Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)


Changed files:
core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java
integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java

diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
index ea1637face8..66d638de248 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
@@ -41,6 +41,7 @@
 import java.util.function.BiPredicate;
 import java.util.function.BooleanSupplier;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.jar.Attributes;
 import java.util.jar.Manifest;
 import java.util.stream.Collectors;
@@ -106,6 +107,8 @@ public class JarResultBuildStep {
             "META-INF/LICENSE",
             "META-INF/LICENSE.txt",
             "META-INF/LICENSE.md",
+            "META-INF/LGPL-3.0.txt",
+            "META-INF/ASL-2.0.txt",
             "META-INF/NOTICE",
             "META-INF/NOTICE.txt",
             "META-INF/NOTICE.md",
@@ -115,7 +118,6 @@ public class JarResultBuildStep {
             "META-INF/DEPENDENCIES",
             "META-INF/DEPENDENCIES.txt",
             "META-INF/beans.xml",
-            "META-INF/io.netty.versions.properties",
             "META-INF/quarkus-config-roots.list",
             "META-INF/quarkus-javadoc.properties",
             "META-INF/quarkus-extension.properties",
@@ -127,6 +129,14 @@ public class JarResultBuildStep {
             "META-INF/build.metadata", // present in the Red Hat Build of Quarkus
             "LICENSE");
 
+    private static final Predicate<String> CONCATENATED_ENTRIES_PREDICATE = new Predicate<>() {
+        @Override
+        public boolean test(String path) {
+            return "META-INF/io.netty.versions.properties".equals(path) ||
+                    (path.startsWith("META-INF/services/") && path.length() > 18);
+        }
+    };
+
     private static final Logger log = Logger.getLogger(JarResultBuildStep.class);
     // we shouldn't have to specify these flags when opening a ZipFS (since they are the default ones), but failure to do so
     // makes a subsequent uberJar creation fail in java 8 (but works fine in Java 11)
@@ -296,7 +306,7 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem,
 
             final Map<String, String> seen = new HashMap<>();
             final Map<String, Set<AppDependency>> duplicateCatcher = new HashMap<>();
-            final Map<String, List<byte[]>> services = new HashMap<>();
+            final Map<String, List<byte[]>> concatenatedEntries = new HashMap<>();
             Set<String> finalIgnoredEntries = new HashSet<>(IGNORED_ENTRIES);
             packageConfig.userConfiguredIgnoredEntries.ifPresent(finalIgnoredEntries::addAll);
 
@@ -323,13 +333,13 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem,
                     if (!Files.isDirectory(resolvedDep)) {
                         try (FileSystem artifactFs = ZipUtils.newFileSystem(resolvedDep)) {
                             for (final Path root : artifactFs.getRootDirectories()) {
-                                walkFileDependencyForDependency(root, runnerZipFs, seen, duplicateCatcher, services,
+                                walkFileDependencyForDependency(root, runnerZipFs, seen, duplicateCatcher, concatenatedEntries,
                                         finalIgnoredEntries, appDep, transformedFromThisArchive);
                             }
                         }
                     } else {
                         walkFileDependencyForDependency(resolvedDep, runnerZipFs, seen, duplicateCatcher,
-                                services, finalIgnoredEntries, appDep, transformedFromThisArchive);
+                                concatenatedEntries, finalIgnoredEntries, appDep, transformedFromThisArchive);
                     }
                 }
             }
@@ -342,7 +352,8 @@ private void buildUberJar0(CurateOutcomeBuildItem curateOutcomeBuildItem,
                     }
                 }
             }
-            copyCommonContent(runnerZipFs, services, applicationArchivesBuildItem, transformedClasses, generatedClasses,
+            copyCommonContent(runnerZipFs, concatenatedEntries, applicationArchivesBuildItem, transformedClasses,
+                    generatedClasses,
                     generatedResources, seen, finalIgnoredEntries);
         }
 
@@ -376,7 +387,7 @@ private static boolean includeAppDep(AppDependency appDep, Optional<Set<AppArtif
     }
 
     private void walkFileDependencyForDependency(Path root, FileSystem runnerZipFs, Map<String, String> seen,
-            Map<String, Set<AppDependency>> duplicateCatcher, Map<String, List<byte[]>> services,
+            Map<String, Set<AppDependency>> duplicateCatcher, Map<String, List<byte[]>> concatenatedEntries,
             Set<String> finalIgnoredEntries, AppDependency appDep, Set<String> transformedFromThisArchive) throws IOException {
         final Path metaInfDir = root.resolve("META-INF");
         Files.walkFileTree(root, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
@@ -409,8 +420,8 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                         boolean transformed = transformedFromThisArchive != null
                                 && transformedFromThisArchive.contains(relativePath);
                         if (!transformed) {
-                            if (relativePath.startsWith("META-INF/services/") && relativePath.length() > 18) {
-                                services.computeIfAbsent(relativePath, (u) -> new ArrayList<>())
+                            if (CONCATENATED_ENTRIES_PREDICATE.test(relativePath)) {
+                                concatenatedEntries.computeIfAbsent(relativePath, (u) -> new ArrayList<>())
                                         .add(Files.readAllBytes(file));
                                 return FileVisitResult.CONTINUE;
                             } else if (!finalIgnoredEntries.contains(relativePath)) {
@@ -420,11 +431,6 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                                     seen.put(relativePath, appDep.toString());
                                     Files.copy(file, runnerZipFs.getPath(relativePath),
                                             StandardCopyOption.REPLACE_EXISTING);
-                                } else if (!relativePath.endsWith(".class")) {
-                                    //for .class entries we warn as a group
-                                    log.warn("Duplicate entry " + relativePath + " entry from " + appDep
-                                            + " will be ignored. Existing file was provided by "
-                                            + seen.get(relativePath));
                                 }
                             }
                         }
@@ -1066,7 +1072,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
         }
     }
 
-    private void copyCommonContent(FileSystem runnerZipFs, Map<String, List<byte[]>> services,
+    private void copyCommonContent(FileSystem runnerZipFs, Map<String, List<byte[]>> concatenatedEntries,
             ApplicationArchivesBuildItem appArchives, TransformedClassesBuildItem transformedClassesBuildItem,
             List<GeneratedClassBuildItem> generatedClasses,
             List<GeneratedResourceBuildItem> generatedResources, Map<String, String> seen,
@@ -1110,8 +1116,8 @@ private void copyCommonContent(FileSystem runnerZipFs, Map<String, List<byte[]>>
             if (Files.exists(target)) {
                 continue;
             }
-            if (i.getName().startsWith("META-INF/services")) {
-                services.computeIfAbsent(i.getName(), (u) -> new ArrayList<>()).add(i.getClassData());
+            if (i.getName().startsWith("META-INF/services/")) {
+                concatenatedEntries.computeIfAbsent(i.getName(), (u) -> new ArrayList<>()).add(i.getClassData());
             } else {
                 try (final OutputStream os = wrapForJDK8232879(Files.newOutputStream(target, DEFAULT_OPEN_OPTIONS))) {
                     os.write(i.getClassData());
@@ -1120,10 +1126,10 @@ private void copyCommonContent(FileSystem runnerZipFs, Map<String, List<byte[]>>
         }
 
         for (Path root : appArchives.getRootArchive().getRootDirs()) {
-            copyFiles(root, runnerZipFs, services, ignoredEntries);
+            copyFiles(root, runnerZipFs, concatenatedEntries, ignoredEntries);
         }
 
-        for (Map.Entry<String, List<byte[]>> entry : services.entrySet()) {
+        for (Map.Entry<String, List<byte[]>> entry : concatenatedEntries.entrySet()) {
             try (final OutputStream os = wrapForJDK8232879(
                     Files.newOutputStream(runnerZipFs.getPath(entry.getKey()), DEFAULT_OPEN_OPTIONS))) {
                 for (byte[] i : entry.getValue()) {
diff --git a/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java
index 71579370db6..b7e0454e912 100644
--- a/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java
+++ b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/main/java/org/acme/reactive/routes/MyDeclarativeRoutes.java
@@ -2,6 +2,7 @@
 
 import javax.enterprise.context.ApplicationScoped;
 
+import io.netty.util.Version;
 import io.quarkus.vertx.web.Route;
 import io.vertx.ext.web.RoutingContext;
 
@@ -23,4 +24,11 @@ public void greetings(RoutingContext rc) {
         }
         rc.response().end("hello " + name);
     }
+
+    @Route(path = "/netty-version", methods = HttpMethod.GET)
+    public void nettyVersion(RoutingContext rc) {
+        rc.response().end(Version.identify().containsKey("netty-common") + ";" +
+                Version.identify().containsKey("netty-handler") + ";" +
+                Version.identify().containsKey("netty-codec"));
+    }
 }
diff --git a/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java
index e1449dd7696..bd4e8aab2d4 100644
--- a/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java
+++ b/integration-tests/maven/src/test/resources/projects/reactive-routes/src/test/java/org/acme/reactive/routes/RouteTest.java
@@ -26,6 +26,10 @@ public void testDeclarativeRoutes() {
                 .header("X-Header", "intercepting the request")
                 .statusCode(200)
                 .body(is("hello quarkus"));
+
+        RestAssured.get("/netty-version").then()
+                .statusCode(200)
+                .body(is("true;true;true"));
     }
 
 }