Skip to content

Commit 18a7c27

Browse files
authored
Load ImageNameSubstitutor from Service Loaders mechanism (#8866)
Add additional mechanism to load `ImageNameSubstitutor`.
1 parent 90b098b commit 18a7c27

File tree

3 files changed

+96
-19
lines changed

3 files changed

+96
-19
lines changed

core/src/main/java/org/testcontainers/utility/ImageNameSubstitutor.java

+34-19
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import lombok.extern.slf4j.Slf4j;
55
import org.testcontainers.UnstableAPI;
66

7+
import java.util.ServiceLoader;
78
import java.util.function.Function;
9+
import java.util.stream.StreamSupport;
810

911
/**
1012
* An image name substitutor converts a Docker image name, as may be specified in code, to an alternative name.
@@ -25,26 +27,19 @@ public abstract class ImageNameSubstitutor implements Function<DockerImageName,
2527
static ImageNameSubstitutor defaultImplementation = new DefaultImageNameSubstitutor();
2628

2729
public static synchronized ImageNameSubstitutor instance() {
30+
return instance(Thread.currentThread().getContextClassLoader());
31+
}
32+
33+
@VisibleForTesting
34+
static synchronized ImageNameSubstitutor instance(ClassLoader classLoader) {
2835
if (instance == null) {
29-
final String configuredClassName = TestcontainersConfiguration.getInstance().getImageSubstitutorClassName();
30-
31-
if (configuredClassName != null) {
32-
log.debug("Attempting to instantiate an ImageNameSubstitutor with class: {}", configuredClassName);
33-
ImageNameSubstitutor configuredInstance;
34-
try {
35-
configuredInstance =
36-
(ImageNameSubstitutor) Thread
37-
.currentThread()
38-
.getContextClassLoader()
39-
.loadClass(configuredClassName)
40-
.getConstructor()
41-
.newInstance();
42-
} catch (Exception e) {
43-
throw new IllegalArgumentException(
44-
"Configured Image Substitutor could not be loaded: " + configuredClassName,
45-
e
46-
);
47-
}
36+
ImageNameSubstitutor configuredInstance = getImageNameSubstitutor(classLoader);
37+
38+
if (configuredInstance != null) {
39+
log.debug(
40+
"Attempting to instantiate an ImageNameSubstitutor with class: {}",
41+
configuredInstance.getClass().getCanonicalName()
42+
);
4843

4944
log.info("Found configured ImageNameSubstitutor: {}", configuredInstance.getDescription());
5045

@@ -63,6 +58,26 @@ public static synchronized ImageNameSubstitutor instance() {
6358
return instance;
6459
}
6560

61+
private static ImageNameSubstitutor getImageNameSubstitutor(ClassLoader classLoader) {
62+
final String configuredClassName = TestcontainersConfiguration.getInstance().getImageSubstitutorClassName();
63+
64+
if (configuredClassName != null) {
65+
try {
66+
return (ImageNameSubstitutor) classLoader.loadClass(configuredClassName).getConstructor().newInstance();
67+
} catch (Exception e) {
68+
throw new IllegalArgumentException(
69+
"Configured Image Substitutor could not be loaded: " + configuredClassName,
70+
e
71+
);
72+
}
73+
}
74+
75+
return StreamSupport
76+
.stream(ServiceLoader.load(ImageNameSubstitutor.class, classLoader).spliterator(), false)
77+
.findFirst()
78+
.orElse(null);
79+
}
80+
6681
public static ImageNameSubstitutor noop() {
6782
return new NoopImageNameSubstitutor();
6883
}

core/src/test/java/org/testcontainers/utility/ImageNameSubstitutorTest.java

+55
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,27 @@
44
import org.junit.Before;
55
import org.junit.Rule;
66
import org.junit.Test;
7+
import org.junit.rules.TemporaryFolder;
78
import org.mockito.Mockito;
89
import org.testcontainers.containers.GenericContainer;
910

11+
import java.io.FileWriter;
12+
import java.io.IOException;
13+
import java.net.URL;
14+
import java.net.URLClassLoader;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.nio.file.Paths;
18+
1019
import static org.assertj.core.api.Assertions.assertThat;
1120
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1221
import static org.mockito.ArgumentMatchers.eq;
1322

1423
public class ImageNameSubstitutorTest {
1524

25+
@Rule
26+
public TemporaryFolder tempFolder = new TemporaryFolder();
27+
1628
@Rule
1729
public MockTestcontainersConfigurationRule config = new MockTestcontainersConfigurationRule();
1830

@@ -81,4 +93,47 @@ public void testImageNameSubstitutorToString() {
8193
);
8294
}
8395
}
96+
97+
@Test
98+
public void testImageNameSubstitutorFromServiceLoader() throws IOException {
99+
Path tempDir = this.tempFolder.newFolder("image-name-substitutor-test").toPath();
100+
Path metaInfDir = Paths.get(tempDir.toString(), "META-INF", "services");
101+
Files.createDirectories(metaInfDir);
102+
103+
createClassFile(tempDir, "org/testcontainers/utility/ImageNameSubstitutor.class", ImageNameSubstitutor.class);
104+
createClassFile(tempDir, "org/testcontainers/utility/FakeImageSubstitutor.class", FakeImageSubstitutor.class);
105+
106+
// Create service provider configuration file
107+
createServiceProviderFile(
108+
metaInfDir,
109+
"org.testcontainers.utility.ImageNameSubstitutor",
110+
"org.testcontainers.utility.FakeImageSubstitutor"
111+
);
112+
113+
URL[] urls = { tempDir.toUri().toURL() };
114+
URLClassLoader classLoader = new URLClassLoader(urls, ImageNameSubstitutorTest.class.getClassLoader());
115+
116+
final ImageNameSubstitutor imageNameSubstitutor = ImageNameSubstitutor.instance(classLoader);
117+
118+
DockerImageName result = imageNameSubstitutor.apply(DockerImageName.parse("original"));
119+
assertThat(result.asCanonicalNameString())
120+
.as("the image has been substituted by default then configured implementations")
121+
.isEqualTo("transformed-substituted-image:latest");
122+
}
123+
124+
private void createClassFile(Path tempDir, String classFilePath, Class<?> clazz) throws IOException {
125+
Path classFile = Paths.get(tempDir.toString(), classFilePath);
126+
Files.createDirectories(classFile.getParent());
127+
Files.copy(clazz.getResourceAsStream("/" + classFilePath), classFile);
128+
}
129+
130+
private void createServiceProviderFile(Path metaInfDir, String serviceInterface, String... implementations)
131+
throws IOException {
132+
Path serviceFile = Paths.get(metaInfDir.toString(), serviceInterface);
133+
try (FileWriter writer = new FileWriter(serviceFile.toFile())) {
134+
for (String impl : implementations) {
135+
writer.write(impl + "\n");
136+
}
137+
}
138+
}
84139
}

docs/features/image_name_substitution.md

+7
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ Note that it is also possible to provide this same configuration property:
125125

126126
Please see [the documentation on configuration mechanisms](./configuration.md) for more information.
127127

128+
Also, you can use the `ServiceLoader` mechanism to provide the fully qualified class name of the `ImageNameSubstitutor` implementation:
129+
130+
=== "`src/test/resources/META-INF/services/org.testcontainers.utility.ImageNameSubstitutor`"
131+
```text
132+
com.mycompany.testcontainers.ExampleImageNameSubstitutor
133+
```
134+
128135

129136
## Overriding image names individually in configuration
130137

0 commit comments

Comments
 (0)