Skip to content

Commit 8ffbfa7

Browse files
authored
[camel-launcher] Fix nested jar url handling- #26039
1 parent 29afe78 commit 8ffbfa7

3 files changed

Lines changed: 168 additions & 54 deletions

File tree

dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/LauncherHelper.java

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
package org.apache.camel.dsl.jbang.core.common;
1818

1919
import java.io.File;
20+
import java.lang.management.ManagementFactory;
21+
import java.net.URI;
2022
import java.net.URL;
21-
import java.net.URLDecoder;
22-
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Path;
2324
import java.util.ArrayList;
2425
import java.util.List;
2526

@@ -46,9 +47,13 @@ public static boolean isRunningFromLauncher() {
4647
return true;
4748
}
4849

49-
// Check JAR path as fallback
50+
// Check filename only — substring match on full path could hit any app embedding camel-jbang-core
5051
String jarPath = getLauncherJarPath();
51-
return jarPath != null && jarPath.contains("camel-launcher");
52+
if (jarPath == null) {
53+
return false;
54+
}
55+
String filename = Path.of(jarPath).getFileName().toString();
56+
return filename.startsWith("camel-launcher");
5257
}
5358

5459
/**
@@ -66,24 +71,45 @@ public static String getLauncherJarPath() {
6671
URL location = LauncherHelper.class.getProtectionDomain()
6772
.getCodeSource().getLocation();
6873
if (location != null) {
69-
String urlStr = location.toString();
70-
// Handle nested JAR (Spring Boot loader)
71-
if (urlStr.startsWith("jar:file:")) {
72-
int idx = urlStr.indexOf("!/");
73-
if (idx > 0) {
74-
String path = urlStr.substring(9, idx);
75-
// Decode URL-encoded characters (spaces, special chars)
76-
return URLDecoder.decode(path, StandardCharsets.UTF_8);
77-
}
74+
return parseJarPath(location.toString());
75+
}
76+
} catch (Exception e) {
77+
System.err.println("WARN: Failed to detect launcher JAR path: " + e.getMessage());
78+
}
79+
return null;
80+
}
81+
82+
/**
83+
* Parses a code-source URL string and returns the filesystem path to the outer JAR. Handles three URL forms:
84+
* <ul>
85+
* <li>{@code jar:nested:/outer.jar/!BOOT-INF/lib/inner.jar!/} — Spring Boot 3.2+/4.x loader</li>
86+
* <li>{@code jar:file:/outer.jar!/BOOT-INF/classes/} — Spring Boot 2.x / shade plugin</li>
87+
* <li>{@code file:/path/to/app.jar} — direct file URL</li>
88+
* </ul>
89+
* Uses {@link URI}-based path decoding to correctly handle percent-encoded characters and Windows drive-letter
90+
* paths (e.g. {@code /C:/...} → {@code C:\...}). {@code indexOf("/!")} is used rather than {@code lastIndexOf} so
91+
* that a JAR whose path itself contains {@code /!} (unlikely but possible) does not lose its prefix.
92+
*/
93+
static String parseJarPath(String urlStr) {
94+
try {
95+
if (urlStr.startsWith("jar:nested:")) {
96+
// Spring Boot 3.2+/4.x: jar:nested:/outer.jar/!BOOT-INF/lib/inner.jar!/
97+
String path = urlStr.substring("jar:nested:".length());
98+
int idx = path.indexOf("/!");
99+
if (idx > 0) {
100+
return Path.of(URI.create("file:" + path.substring(0, idx))).toString();
78101
}
79-
// Handle direct file URL
80-
if (urlStr.startsWith("file:")) {
81-
String path = urlStr.substring(5);
82-
return URLDecoder.decode(path, StandardCharsets.UTF_8);
102+
} else if (urlStr.startsWith("jar:file:")) {
103+
// Spring Boot 2.x / shade plugin: jar:file:/outer.jar!/BOOT-INF/classes/
104+
int idx = urlStr.indexOf("!/");
105+
if (idx > 0) {
106+
return Path.of(URI.create(urlStr.substring("jar:".length(), idx))).toString();
83107
}
108+
} else if (urlStr.startsWith("file:")) {
109+
return Path.of(URI.create(urlStr)).toString();
84110
}
85111
} catch (Exception e) {
86-
System.err.println("WARN: Failed to detect launcher JAR path: " + e.getMessage());
112+
System.err.println("WARN: Failed to parse JAR path from URL '" + urlStr + "': " + e.getMessage());
87113
}
88114
return null;
89115
}
@@ -98,10 +124,25 @@ public static List<String> getCamelCommand() {
98124
String jarPath = getLauncherJarPath();
99125
if (jarPath != null) {
100126
cmds.add(getJavaCommand());
127+
// Forward -D and -X JVM arguments so child processes inherit proxy, truststore,
128+
// and memory settings. Skips -javaagent/-agentlib flags to avoid port conflicts.
129+
ManagementFactory.getRuntimeMXBean().getInputArguments().stream()
130+
.filter(arg -> arg.startsWith("-D") || arg.startsWith("-X"))
131+
.forEach(cmds::add);
101132
cmds.add("-jar");
102133
cmds.add(jarPath);
103134
return cmds;
104135
}
136+
// Launcher detected but JAR path unresolvable — log raw URL to aid diagnosis
137+
try {
138+
URL location = LauncherHelper.class.getProtectionDomain().getCodeSource().getLocation();
139+
System.err.println(
140+
"WARN: Running from launcher but JAR path could not be resolved; falling back to 'camel'. Code-source URL: "
141+
+ location);
142+
} catch (Exception ignored) {
143+
System.err.println(
144+
"WARN: Running from launcher but JAR path could not be resolved; falling back to 'camel'.");
145+
}
105146
}
106147

107148
// Fall back to JBang-style command
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.camel.dsl.jbang.core.common;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.condition.EnabledOnOs;
21+
import org.junit.jupiter.api.condition.OS;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
class LauncherHelperTest {
26+
27+
// jar:nested: — Spring Boot 3.2+/4.x loader
28+
// Real URL form: jar:nested:/outer.jar/!BOOT-INF/lib/inner.jar!/
29+
// The outer-jar boundary is /! (slash-bang), not !/ (bang-slash).
30+
31+
@Test
32+
void parsesNestedJarUrlOnLinux() {
33+
String result = LauncherHelper.parseJarPath(
34+
"jar:nested:/home/user/camel-launcher-4.23.0.jar/!BOOT-INF/lib/camel-jbang-core.jar!/");
35+
assertThat(result).isEqualTo("/home/user/camel-launcher-4.23.0.jar");
36+
}
37+
38+
@Test
39+
void parsesNestedJarUrlWithPercentEncodedSpaces() {
40+
String result = LauncherHelper.parseJarPath(
41+
"jar:nested:/home/user/my%20app/camel-launcher.jar/!BOOT-INF/lib/camel-jbang-core.jar!/");
42+
assertThat(result).isEqualTo("/home/user/my app/camel-launcher.jar");
43+
}
44+
45+
@Test
46+
void parsesNestedJarUrlWithWindowsDriveLetter() {
47+
// Platform-neutral: verify the outer jar filename is extracted regardless of OS path format
48+
String result = LauncherHelper.parseJarPath(
49+
"jar:nested:/C:/Users/user/camel-launcher-4.23.0.jar/!BOOT-INF/lib/camel-jbang-core.jar!/");
50+
assertThat(result).isNotNull().endsWith("camel-launcher-4.23.0.jar");
51+
}
52+
53+
@Test
54+
@EnabledOnOs(OS.WINDOWS)
55+
void parsesNestedJarUrlWindowsDriveLetterStripsLeadingSlash() {
56+
// On Windows, Path.of(URI) strips the /C:/ prefix — verify java -jar can use the result
57+
String result = LauncherHelper.parseJarPath(
58+
"jar:nested:/C:/Users/user/camel-launcher-4.23.0.jar/!BOOT-INF/lib/camel-jbang-core.jar!/");
59+
assertThat(result).doesNotStartWith("/C:");
60+
}
61+
62+
// jar:file: — Spring Boot 2.x / shade plugin
63+
64+
@Test
65+
void parsesJarFileUrlOnLinux() {
66+
String result = LauncherHelper.parseJarPath(
67+
"jar:file:/home/user/camel-launcher-4.23.0.jar!/BOOT-INF/classes/");
68+
assertThat(result).isEqualTo("/home/user/camel-launcher-4.23.0.jar");
69+
}
70+
71+
@Test
72+
void parsesJarFileUrlWithPercentEncodedSpaces() {
73+
String result = LauncherHelper.parseJarPath(
74+
"jar:file:/home/user/my%20tools/camel-launcher.jar!/BOOT-INF/classes/");
75+
assertThat(result).isEqualTo("/home/user/my tools/camel-launcher.jar");
76+
}
77+
78+
// file: — direct file URL
79+
80+
@Test
81+
void parsesFileUrl() {
82+
String result = LauncherHelper.parseJarPath("file:/home/user/camel-launcher-4.23.0.jar");
83+
assertThat(result).isEqualTo("/home/user/camel-launcher-4.23.0.jar");
84+
}
85+
86+
@Test
87+
void parsesFileUrlWithPercentEncodedSpaces() {
88+
String result = LauncherHelper.parseJarPath("file:/home/user/my%20tools/camel-launcher.jar");
89+
assertThat(result).isEqualTo("/home/user/my tools/camel-launcher.jar");
90+
}
91+
92+
// edge cases
93+
94+
@Test
95+
void returnsNullForUnknownScheme() {
96+
assertThat(LauncherHelper.parseJarPath("http://example.com/camel-launcher.jar")).isNull();
97+
}
98+
99+
@Test
100+
void returnsNullForNestedJarUrlWithoutBoundarySeparator() {
101+
// Cannot determine outer JAR boundary without /!
102+
assertThat(LauncherHelper.parseJarPath("jar:nested:/home/user/camel-launcher.jar")).isNull();
103+
}
104+
}

dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncher.java

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@
1616
*/
1717
package org.apache.camel.dsl.jbang.launcher;
1818

19-
import java.net.URL;
20-
import java.net.URLDecoder;
21-
import java.nio.charset.StandardCharsets;
19+
import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
2220

2321
/**
2422
* Main class for the Camel CLI Fat-Jar Launcher.
@@ -34,46 +32,17 @@ public class CamelLauncher {
3432
* @param args command line arguments to pass to Camel CLI
3533
*/
3634
public static void main(String... args) {
37-
// Set system property to indicate we're running from the launcher
38-
System.setProperty("camel.launcher", "true");
35+
System.setProperty(LauncherHelper.CAMEL_LAUNCHER_PROPERTY, "true");
3936

40-
// Try to determine and set the JAR path
41-
String jarPath = detectJarPath();
37+
// Resolve JAR path via the shared helper so all downstream code uses one implementation
38+
String jarPath = LauncherHelper.getLauncherJarPath();
4239
if (jarPath != null) {
43-
System.setProperty("camel.launcher.jar", jarPath);
40+
System.setProperty(LauncherHelper.CAMEL_LAUNCHER_JAR_PROPERTY, jarPath);
4441
}
4542

4643
CamelLauncherMain main = new CamelLauncherMain();
4744
// allow to use 3rd-party plugins
4845
main.setDiscoverPlugins(true);
4946
main.execute(args);
5047
}
51-
52-
private static String detectJarPath() {
53-
try {
54-
URL location = CamelLauncher.class.getProtectionDomain()
55-
.getCodeSource().getLocation();
56-
if (location != null) {
57-
String urlStr = location.toString();
58-
String path = null;
59-
// Handle nested JAR (Spring Boot loader)
60-
if (urlStr.startsWith("jar:file:")) {
61-
int idx = urlStr.indexOf("!/");
62-
if (idx > 0) {
63-
path = urlStr.substring("jar:file:".length(), idx);
64-
}
65-
} else if (urlStr.startsWith("file:")) {
66-
// Handle direct file URL
67-
path = urlStr.substring("file:".length());
68-
}
69-
if (path != null) {
70-
// Decode URL-encoded characters (spaces, special chars)
71-
return URLDecoder.decode(path, StandardCharsets.UTF_8);
72-
}
73-
}
74-
} catch (Exception e) {
75-
System.err.println("WARN: Failed to detect launcher JAR path: " + e.getMessage());
76-
}
77-
return null;
78-
}
7948
}

0 commit comments

Comments
 (0)