Skip to content

Commit 47f8f7c

Browse files
Merge pull request #2560 from chathuranga-jayanath-99/fix-file-connector-issue-master
Fix duplicate VFS provider registration when multiple CARs bundle the same connector dependency
2 parents 850a5bf + b799a71 commit 47f8f7c

3 files changed

Lines changed: 345 additions & 3 deletions

File tree

modules/core/src/main/java/org/apache/synapse/libraries/LibClassLoader.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ public void addToClassPath(String path) throws MalformedURLException {
4848

4949
File file = new File(path);
5050
ArrayList urls = new ArrayList();
51-
urls.add(file.toURL());
51+
if (file.exists()) {
52+
urls.add(file.toURL());
53+
}
5254
File libfiles = new File(file, "lib");
5355
if (!addFiles(urls, libfiles)) {
5456
libfiles = new File(file, "Lib");

modules/core/src/main/java/org/apache/synapse/libraries/util/LibDeployerUtils.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,14 @@ public static Library createSynapseLibrary(String libPath, DeploymentFileData de
9191
} else {
9292
if (deployedLibClassLoader instanceof LibClassLoader) {
9393
try {
94-
((LibClassLoader) deployedLibClassLoader).addToClassPath(extractPath);
94+
if (!isDependencyAlreadyLoaded((LibClassLoader) deployedLibClassLoader, extractPath)) {
95+
((LibClassLoader) deployedLibClassLoader).addToClassPath(extractPath);
96+
} else {
97+
if (log.isDebugEnabled()) {
98+
log.debug("Synapse Library '" + libArtifactName +
99+
"' is already loaded in the class loader. Skipping duplicate addition of: " + extractPath);
100+
}
101+
}
95102
} catch (MalformedURLException e) {
96103
throw new SynapseArtifactDeploymentException("Error setting up lib classpath for Synapse" +
97104
" Library : " + libFile.getAbsolutePath(), e);
@@ -133,7 +140,15 @@ public static void addDependencyToSynapseLibrary(String libraryName, String depe
133140
if (classLoader instanceof LibClassLoader) {
134141
LibClassLoader libClassLoader = (LibClassLoader) classLoader;
135142
try {
136-
libClassLoader.addToClassPath(dependencyPath);
143+
if (!isDependencyAlreadyLoaded(libClassLoader, dependencyPath)) {
144+
libClassLoader.addToClassPath(dependencyPath);
145+
} else {
146+
if (log.isDebugEnabled()) {
147+
log.debug("Dependency '" + new File(dependencyPath).getName() +
148+
"' is already loaded in the class loader for Synapse Library '" +
149+
libraryName + "'. Skipping duplicate addition.");
150+
}
151+
}
137152
} catch (MalformedURLException e) {
138153
throw new SynapseArtifactDeploymentException("Error while adding dependency to the Synapse Library : " +
139154
libraryName, e);
@@ -593,6 +608,25 @@ private static void copyInputStream(InputStream in, OutputStream out)
593608

594609
/////////////////// End Of Common Utility Methods
595610

611+
/**
612+
* Checks whether a dependency (identified by its file name) is already loaded
613+
* in the given LibClassLoader. This guards against duplicate class-path entries
614+
* when multiple CARs bundle the same connector dependency jar.
615+
*
616+
* @param libClassLoader the class loader to inspect
617+
* @param dependencyPath absolute path to the dependency jar or directory
618+
* @return true if a URL with the same file name is already present
619+
*/
620+
private static boolean isDependencyAlreadyLoaded(LibClassLoader libClassLoader, String dependencyPath) {
621+
String dependencyName = new File(dependencyPath).getName();
622+
for (URL url : libClassLoader.getURLs()) {
623+
File urlFile = new File(url.getPath());
624+
if (dependencyName.equals(urlFile.getName())) {
625+
return true;
626+
}
627+
}
628+
return false;
629+
}
596630

597631
public static void main(String[] args) {
598632
new SynapseLibrary(null, null).resolveDependencies(null);
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/*
2+
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* 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,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
package org.apache.synapse.libraries.util;
19+
20+
import org.apache.axis2.deployment.DeploymentException;
21+
import org.apache.synapse.config.SynapseConfiguration;
22+
import org.apache.synapse.libraries.LibClassLoader;
23+
import org.junit.After;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
27+
import java.io.File;
28+
import java.io.IOException;
29+
import java.net.URL;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
33+
import static org.junit.Assert.*;
34+
35+
/**
36+
* Unit tests for {@link LibDeployerUtils#addDependencyToSynapseLibrary(String, String)}.
37+
* <p>
38+
* Covers the deduplication fix for issue #4965: when multiple CARs each bundle the same
39+
* connector dependency JAR (e.g., {@code commons-vfs2-sandbox-2.10.0-wso2v4.jar}), the
40+
* second CAR's deployment must <em>not</em> add a duplicate URL to the shared
41+
* {@link LibClassLoader}. Without this dedup, {@code StandardFileSystemManager.configurePlugins()}
42+
* encounters two copies of {@code META-INF/vfs-providers.xml} and throws
43+
* {@code FileSystemException: Multiple providers registered for URL scheme "smb2"},
44+
* which opens the circuit breaker and permanently breaks File Connector operations.
45+
*/
46+
public class LibDeployerUtilsTest {
47+
48+
private static final String LIBRARY_NAME = "{org.wso2.carbon.connector}file";
49+
50+
private Path car1Dir;
51+
private Path car2Dir;
52+
private Path car3Dir;
53+
54+
@Before
55+
public void setUp() throws IOException {
56+
// Ensure the static SynapseConfiguration classloader registry is clean before each test.
57+
SynapseConfiguration.getLibraryClassLoaders().clear();
58+
car1Dir = Files.createTempDirectory("issue4965_car1_");
59+
car2Dir = Files.createTempDirectory("issue4965_car2_");
60+
car3Dir = Files.createTempDirectory("issue4965_car3_");
61+
}
62+
63+
@After
64+
public void tearDown() {
65+
SynapseConfiguration.getLibraryClassLoaders().clear();
66+
deleteQuietly(car1Dir.toFile());
67+
deleteQuietly(car2Dir.toFile());
68+
deleteQuietly(car3Dir.toFile());
69+
}
70+
71+
// -------------------------------------------------------------------------
72+
// Basic classloader lifecycle
73+
// -------------------------------------------------------------------------
74+
75+
/**
76+
* Verifies that the first call to {@code addDependencyToSynapseLibrary} creates a
77+
* {@link LibClassLoader} and registers it in {@link SynapseConfiguration}.
78+
*/
79+
@Test
80+
public void testFirstCallCreatesLibClassLoader() throws DeploymentException, IOException {
81+
String libName = "{org.wso2.carbon.connector}newlib";
82+
File jar = createTempFile(car1Dir, "newlib-1.0.0.jar");
83+
84+
assertNull("No classloader should exist before the first call",
85+
SynapseConfiguration.getClassLoader(libName));
86+
87+
LibDeployerUtils.addDependencyToSynapseLibrary(libName, jar.getAbsolutePath());
88+
89+
ClassLoader loader = SynapseConfiguration.getClassLoader(libName);
90+
assertNotNull("ClassLoader must be created on the first call", loader);
91+
assertTrue("ClassLoader must be a LibClassLoader instance",
92+
loader instanceof LibClassLoader);
93+
}
94+
95+
/**
96+
* Verifies that the first call adds the dependency JAR to the classloader URL list.
97+
*/
98+
@Test
99+
public void testFirstCallAddsJarUrlToClassLoader() throws DeploymentException, IOException {
100+
File jar = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
101+
102+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar.getAbsolutePath());
103+
104+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
105+
assertEquals("First call must add exactly one URL to the classloader", 1,
106+
classLoader.getURLs().length);
107+
}
108+
109+
// -------------------------------------------------------------------------
110+
// Deduplication — issue #4965 regression tests
111+
// -------------------------------------------------------------------------
112+
113+
/**
114+
* Core regression test for issue #4965.
115+
* <p>
116+
* Simulates two CARs each bundling {@code commons-vfs2-sandbox-2.10.0-wso2v4.jar}: the JARs
117+
* have the same filename but live under different CAR temp-extraction paths. After both
118+
* deployments the {@link LibClassLoader} must contain that filename exactly <em>once</em>.
119+
* <p>
120+
* Before the fix the second call added a duplicate URL, causing
121+
* {@code StandardFileSystemManager} to read {@code vfs-providers.xml} twice and throw
122+
* {@code "Multiple providers registered for URL scheme 'smb2'"}.
123+
*/
124+
@Test
125+
public void testDuplicateFilenameIsNotAddedToClassLoader() throws DeploymentException, IOException {
126+
// Two CARs, same dependency filename, different extraction paths
127+
File jar1 = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
128+
File jar2 = createTempFile(car2Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
129+
130+
// CAR 1 deploys — no classloader exists yet
131+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar1.getAbsolutePath());
132+
// CAR 2 deploys — classloader already exists; duplicate filename must be skipped
133+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar2.getAbsolutePath());
134+
135+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
136+
assertNotNull(classLoader);
137+
138+
long matchCount = countUrlsWithFilename(
139+
classLoader.getURLs(), "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
140+
assertEquals(
141+
"Duplicate JAR filename must appear exactly once in the classloader URLs"
142+
+ " — regression guard for issue #4965",
143+
1, matchCount);
144+
}
145+
146+
/**
147+
* Verifies that the deduplication holds across three CARs bundling the same dependency.
148+
*/
149+
@Test
150+
public void testTripleDeploymentDeduplicates() throws DeploymentException, IOException {
151+
File jar1 = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
152+
File jar2 = createTempFile(car2Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
153+
File jar3 = createTempFile(car3Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
154+
155+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar1.getAbsolutePath());
156+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar2.getAbsolutePath());
157+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar3.getAbsolutePath());
158+
159+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
160+
long matchCount = countUrlsWithFilename(
161+
classLoader.getURLs(), "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
162+
assertEquals(
163+
"Same JAR bundled in three CARs should appear exactly once in classloader URLs",
164+
1, matchCount);
165+
}
166+
167+
// -------------------------------------------------------------------------
168+
// Deduplication is filename-based: distinct JARs must still be added
169+
// -------------------------------------------------------------------------
170+
171+
/**
172+
* Verifies that a dependency with a <em>different</em> filename is correctly added as a
173+
* separate classloader entry — i.e., the dedup must not suppress genuinely distinct JARs.
174+
*/
175+
@Test
176+
public void testDifferentFilenameIsAdded() throws DeploymentException, IOException {
177+
File jar1 = createTempFile(car1Dir, "commons-vfs2-2.10.0-wso2v4.jar");
178+
File jar2 = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
179+
180+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar1.getAbsolutePath());
181+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar2.getAbsolutePath());
182+
183+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
184+
URL[] urls = classLoader.getURLs();
185+
assertEquals("Two distinct dependency JARs must each appear once in the classloader", 2,
186+
urls.length);
187+
}
188+
189+
/**
190+
* Full two-CAR scenario with all File Connector 6 dependencies:
191+
* verifies that both {@code commons-vfs2} and {@code commons-vfs2-sandbox} are each
192+
* deduplicated independently when two CARs are deployed.
193+
*/
194+
@Test
195+
public void testAllFileConnectorDependenciesDeduplicatedAcrossTwoCars()
196+
throws DeploymentException, IOException {
197+
// CAR 1 dependencies
198+
File vfsJar1 = createTempFile(car1Dir, "commons-vfs2-2.10.0-wso2v4.jar");
199+
File sandboxJar1 = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
200+
// CAR 2 — same filenames, different temp extraction path
201+
File vfsJar2 = createTempFile(car2Dir, "commons-vfs2-2.10.0-wso2v4.jar");
202+
File sandboxJar2 = createTempFile(car2Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
203+
204+
// CAR 1 deploys
205+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, vfsJar1.getAbsolutePath());
206+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, sandboxJar1.getAbsolutePath());
207+
// CAR 2 deploys the same deps again
208+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, vfsJar2.getAbsolutePath());
209+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, sandboxJar2.getAbsolutePath());
210+
211+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
212+
URL[] urls = classLoader.getURLs();
213+
214+
assertEquals("Total unique dependency JARs should be 2 — no duplicates allowed", 2,
215+
urls.length);
216+
assertEquals("commons-vfs2 JAR must appear exactly once",
217+
1, countUrlsWithFilename(urls, "commons-vfs2-2.10.0-wso2v4.jar"));
218+
assertEquals("commons-vfs2-sandbox JAR must appear exactly once",
219+
1, countUrlsWithFilename(urls, "commons-vfs2-sandbox-2.10.0-wso2v4.jar"));
220+
}
221+
222+
// -------------------------------------------------------------------------
223+
// Edge cases
224+
// -------------------------------------------------------------------------
225+
226+
/**
227+
* Verifies that calling {@code addDependencyToSynapseLibrary} with the exact same path
228+
* twice (not just same filename) also deduplicates correctly.
229+
*/
230+
@Test
231+
public void testExactSamePathDeduplicates() throws DeploymentException, IOException {
232+
File jar = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
233+
String path = jar.getAbsolutePath();
234+
235+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, path);
236+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, path);
237+
238+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
239+
long matchCount = countUrlsWithFilename(
240+
classLoader.getURLs(), "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
241+
assertEquals("Same path added twice must still produce only one URL", 1, matchCount);
242+
}
243+
244+
/**
245+
* Verifies that separate library names maintain independent classloaders, so dedup for
246+
* one library does not interfere with another.
247+
*/
248+
@Test
249+
public void testSeparateLibrariesHaveIndependentClassLoaders()
250+
throws DeploymentException, IOException {
251+
String libA = "{org.wso2.carbon.connector}fileA";
252+
String libB = "{org.wso2.carbon.connector}fileB";
253+
254+
File jarA = createTempFile(car1Dir, "lib-a-1.0.0.jar");
255+
File jarB = createTempFile(car2Dir, "lib-b-1.0.0.jar");
256+
257+
LibDeployerUtils.addDependencyToSynapseLibrary(libA, jarA.getAbsolutePath());
258+
LibDeployerUtils.addDependencyToSynapseLibrary(libB, jarB.getAbsolutePath());
259+
260+
assertNotSame("Each library must have its own independent classloader",
261+
SynapseConfiguration.getClassLoader(libA),
262+
SynapseConfiguration.getClassLoader(libB));
263+
assertEquals("libA classloader must contain exactly 1 URL", 1,
264+
((LibClassLoader) SynapseConfiguration.getClassLoader(libA)).getURLs().length);
265+
assertEquals("libB classloader must contain exactly 1 URL", 1,
266+
((LibClassLoader) SynapseConfiguration.getClassLoader(libB)).getURLs().length);
267+
}
268+
269+
// -------------------------------------------------------------------------
270+
// Helpers
271+
// -------------------------------------------------------------------------
272+
273+
private static File createTempFile(Path directory, String filename) throws IOException {
274+
File file = directory.resolve(filename).toFile();
275+
file.createNewFile();
276+
return file;
277+
}
278+
279+
/**
280+
* Counts how many URLs in the given array end with the specified filename.
281+
*/
282+
private static long countUrlsWithFilename(URL[] urls, String filename) {
283+
long count = 0;
284+
for (URL url : urls) {
285+
String path = url.getPath();
286+
String urlFilename = path.substring(path.lastIndexOf('/') + 1);
287+
if (filename.equals(urlFilename)) {
288+
count++;
289+
}
290+
}
291+
return count;
292+
}
293+
294+
private static void deleteQuietly(File file) {
295+
if (file == null || !file.exists()) {
296+
return;
297+
}
298+
File[] children = file.listFiles();
299+
if (children != null) {
300+
for (File child : children) {
301+
deleteQuietly(child);
302+
}
303+
}
304+
file.delete();
305+
}
306+
}

0 commit comments

Comments
 (0)