|
8 | 8 | import java.lang.reflect.Method; |
9 | 9 | import java.util.ArrayList; |
10 | 10 | import java.util.Arrays; |
| 11 | +import java.util.Collections; |
11 | 12 | import java.util.List; |
12 | 13 | import java.util.concurrent.ConcurrentHashMap; |
13 | | -import java.util.stream.Collectors; |
14 | | -import java.util.stream.Stream; |
| 14 | +import java.util.concurrent.ConcurrentMap; |
| 15 | +import java.util.function.Supplier; |
15 | 16 | import org.slf4j.Logger; |
16 | 17 | import org.slf4j.LoggerFactory; |
| 18 | +import org.testcontainers.containers.GenericContainer; |
17 | 19 | import org.testcontainers.containers.JdbcDatabaseContainer; |
18 | 20 | import org.testcontainers.containers.output.Slf4jLogConsumer; |
19 | 21 | import org.testcontainers.utility.DockerImageName; |
|
22 | 24 | * ContainerFactory is the companion interface to {@link TestDatabase} for providing it with |
23 | 25 | * suitable testcontainer instances. |
24 | 26 | */ |
25 | | -public interface ContainerFactory<C extends JdbcDatabaseContainer<?>> { |
| 27 | +public abstract class ContainerFactory<C extends JdbcDatabaseContainer<?>> { |
26 | 28 |
|
27 | | - /** |
28 | | - * Creates a new, unshared testcontainer instance. This usually wraps the default constructor for |
29 | | - * the testcontainer type. |
30 | | - */ |
31 | | - C createNewContainer(DockerImageName imageName); |
| 29 | + static private final Logger LOGGER = LoggerFactory.getLogger(ContainerFactory.class); |
32 | 30 |
|
33 | | - /** |
34 | | - * Returns the class object of the testcontainer. |
35 | | - */ |
36 | | - Class<?> getContainerClass(); |
| 31 | + private record ContainerKey(Class<? extends ContainerFactory> clazz, DockerImageName imageName, List<String> methods) {}; |
37 | 32 |
|
38 | | - /** |
39 | | - * Returns a shared instance of the testcontainer. |
40 | | - */ |
41 | | - default C shared(String imageName, String... methods) { |
42 | | - final String mapKey = Stream.concat( |
43 | | - Stream.of(imageName, this.getClass().getCanonicalName()), |
44 | | - Stream.of(methods)) |
45 | | - .collect(Collectors.joining("+")); |
46 | | - return Singleton.getOrCreate(mapKey, this); |
47 | | - } |
| 33 | + private static class ContainerOrException { |
48 | 34 |
|
49 | | - /** |
50 | | - * This class is exclusively used by {@link #shared(String, String...)}. It wraps a specific shared |
51 | | - * testcontainer instance, which is created exactly once. |
52 | | - */ |
53 | | - class Singleton<C extends JdbcDatabaseContainer<?>> { |
54 | | - |
55 | | - static private final Logger LOGGER = LoggerFactory.getLogger(Singleton.class); |
56 | | - static private final ConcurrentHashMap<String, Singleton<?>> LAZY = new ConcurrentHashMap<>(); |
| 35 | + private final Supplier<GenericContainer<?>> containerSupplier; |
| 36 | + private volatile RuntimeException _exception = null; |
| 37 | + private volatile GenericContainer<?> _container = null; |
57 | 38 |
|
58 | | - @SuppressWarnings("unchecked") |
59 | | - static private <C extends JdbcDatabaseContainer<?>> C getOrCreate(String mapKey, ContainerFactory<C> factory) { |
60 | | - final Singleton<?> singleton = LAZY.computeIfAbsent(mapKey, Singleton<C>::new); |
61 | | - return ((Singleton<C>) singleton).getOrCreate(factory); |
| 39 | + ContainerOrException(Supplier<GenericContainer<?>> containerSupplier) { |
| 40 | + this.containerSupplier = containerSupplier; |
62 | 41 | } |
63 | 42 |
|
64 | | - final private String imageName; |
65 | | - final private List<String> methodNames; |
66 | | - |
67 | | - private C sharedContainer; |
68 | | - private RuntimeException containerCreationError; |
69 | | - |
70 | | - private Singleton(String imageNamePlusMethods) { |
71 | | - final String[] parts = imageNamePlusMethods.split("\\+"); |
72 | | - this.imageName = parts[0]; |
73 | | - this.methodNames = Arrays.stream(parts).skip(2).toList(); |
| 43 | + private void populate() { |
| 44 | + synchronized (this) { |
| 45 | + if (_container == null && _exception == null) { |
| 46 | + try { |
| 47 | + _container = containerSupplier.get(); |
| 48 | + } catch (RuntimeException e) { |
| 49 | + _exception = e; |
| 50 | + } |
| 51 | + } |
| 52 | + } |
74 | 53 | } |
75 | 54 |
|
76 | | - private synchronized C getOrCreate(ContainerFactory<C> factory) { |
77 | | - if (sharedContainer == null && containerCreationError == null) { |
78 | | - try { |
79 | | - create(imageName, factory, methodNames); |
80 | | - } catch (RuntimeException e) { |
81 | | - sharedContainer = null; |
82 | | - containerCreationError = e; |
83 | | - } |
| 55 | + RuntimeException exception() { |
| 56 | + if (_exception == null && _container == null) { |
| 57 | + populate(); |
84 | 58 | } |
85 | | - if (containerCreationError != null) { |
86 | | - throw new RuntimeException( |
87 | | - "Error during container creation for imageName=" + imageName |
88 | | - + ", factory=" + factory.getClass().getName() |
89 | | - + ", methods=" + methodNames, |
90 | | - containerCreationError); |
| 59 | + return _exception; |
| 60 | + } |
| 61 | + |
| 62 | + GenericContainer<?> container() { |
| 63 | + if (_exception == null && _container == null) { |
| 64 | + populate(); |
91 | 65 | } |
92 | | - return sharedContainer; |
| 66 | + return _container; |
93 | 67 | } |
94 | 68 |
|
95 | | - private void create(String imageName, ContainerFactory<C> factory, List<String> methodNames) { |
| 69 | + } |
| 70 | + |
| 71 | + private static final ConcurrentMap<ContainerKey, ContainerOrException> SHARED_CONTAINERS = new ConcurrentHashMap<>(); |
| 72 | + |
| 73 | + /** |
| 74 | + * Creates a new, unshared testcontainer instance. This usually wraps the default constructor for |
| 75 | + * the testcontainer type. |
| 76 | + */ |
| 77 | + protected abstract C createNewContainer(DockerImageName imageName); |
| 78 | + |
| 79 | + /** |
| 80 | + * Returns a shared instance of the testcontainer. |
| 81 | + */ |
| 82 | + public final C shared(String imageName, String... methods) { |
| 83 | + List<String> methodList = methods == null ? Collections.emptyList() : Arrays.asList(methods); |
| 84 | + DockerImageName dockerImageName = DockerImageName.parse(imageName); |
| 85 | + final ContainerKey containerKey = new ContainerKey(getClass(), dockerImageName, methodList); |
| 86 | + ContainerOrException containerOrError = SHARED_CONTAINERS.computeIfAbsent(containerKey, this::createContainerOrError); |
| 87 | + if (containerOrError.exception() != null) { |
| 88 | + throw containerOrError.exception(); |
| 89 | + } |
| 90 | + return (C) containerOrError.container(); |
| 91 | + } |
| 92 | + |
| 93 | + public final C exclusive(String imageName, String... methods) { |
| 94 | + DockerImageName dockerImageName = DockerImageName.parse(imageName); |
| 95 | + List<String> methodList = methods == null ? Collections.emptyList() : Arrays.asList(methods); |
| 96 | + return (C) createContainerSupplier(dockerImageName, methodList).get(); |
| 97 | + } |
| 98 | + |
| 99 | + private ContainerOrException createContainerOrError(ContainerKey containerKey) { |
| 100 | + DockerImageName imageName = containerKey.imageName(); |
| 101 | + List<String> methodNames = containerKey.methods(); |
| 102 | + return new ContainerOrException(createContainerSupplier(imageName, methodNames)); |
| 103 | + } |
| 104 | + |
| 105 | + private Supplier<GenericContainer<?>> createContainerSupplier(DockerImageName imageName, List<String> methodNames) { |
| 106 | + return () -> { |
96 | 107 | LOGGER.info("Creating new shared container based on {} with {}.", imageName, methodNames); |
97 | 108 | try { |
98 | | - final var parsed = DockerImageName.parse(imageName); |
| 109 | + GenericContainer container = createNewContainer(imageName); |
| 110 | + |
99 | 111 | final var methods = new ArrayList<Method>(); |
100 | 112 | for (String methodName : methodNames) { |
101 | | - methods.add(factory.getClass().getMethod(methodName, factory.getContainerClass())); |
| 113 | + methods.add(getClass().getMethod(methodName, container.getClass())); |
102 | 114 | } |
103 | | - sharedContainer = factory.createNewContainer(parsed); |
104 | | - sharedContainer.withLogConsumer(new Slf4jLogConsumer(LOGGER)); |
| 115 | + container.withLogConsumer(new Slf4jLogConsumer(LOGGER)); |
105 | 116 | for (Method method : methods) { |
106 | 117 | LOGGER.info("Calling {} in {} on new shared container based on {}.", |
107 | | - method.getName(), factory.getClass().getName(), imageName); |
108 | | - method.invoke(factory, sharedContainer); |
| 118 | + method.getName(), getClass().getName(), imageName); |
| 119 | + method.invoke(this, container); |
109 | 120 | } |
110 | | - sharedContainer.start(); |
| 121 | + container.start(); |
| 122 | + return container; |
111 | 123 | } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { |
112 | 124 | throw new RuntimeException(e); |
113 | 125 | } |
114 | | - } |
115 | | - |
| 126 | + }; |
116 | 127 | } |
117 | 128 |
|
118 | 129 | } |
0 commit comments