@@ -303,7 +337,7 @@
org.apache.maven.plugins
maven-pmd-plugin
- 3.26.0
+ 3.27.0
true
true
@@ -315,12 +349,12 @@
net.sourceforge.pmd
pmd-core
- 7.14.0
+ 7.15.0
net.sourceforge.pmd
pmd-java
- 7.14.0
+ 7.15.0
diff --git a/tci-base/src/main/java/software/xdev/tci/TCI.java b/base/src/main/java/software/xdev/tci/TCI.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/TCI.java
rename to base/src/main/java/software/xdev/tci/TCI.java
diff --git a/base/src/main/java/software/xdev/tci/envperf/EnvironmentPerformance.java b/base/src/main/java/software/xdev/tci/envperf/EnvironmentPerformance.java
new file mode 100644
index 00000000..75bfb4f2
--- /dev/null
+++ b/base/src/main/java/software/xdev/tci/envperf/EnvironmentPerformance.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright © 2024 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.envperf;
+
+import software.xdev.tci.envperf.impl.TCIEnvironmentPerformance;
+import software.xdev.tci.serviceloading.TCIServiceLoader;
+
+
+/**
+ * Describes the Performance of the Environment where TCI is running.
+ */
+public final class EnvironmentPerformance
+{
+ /**
+ * @see TCIEnvironmentPerformance#cpuSlownessFactor()
+ */
+ public static int cpuSlownessFactor()
+ {
+ return impl().cpuSlownessFactor();
+ }
+
+ public static TCIEnvironmentPerformance impl()
+ {
+ return TCIServiceLoader.instance().service(TCIEnvironmentPerformance.class);
+ }
+
+ private EnvironmentPerformance()
+ {
+ }
+}
diff --git a/base/src/main/java/software/xdev/tci/envperf/impl/DefaultTCIEnvironmentPerformance.java b/base/src/main/java/software/xdev/tci/envperf/impl/DefaultTCIEnvironmentPerformance.java
new file mode 100644
index 00000000..a1743afc
--- /dev/null
+++ b/base/src/main/java/software/xdev/tci/envperf/impl/DefaultTCIEnvironmentPerformance.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2024 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.envperf.impl;
+
+import java.util.Optional;
+
+import org.slf4j.LoggerFactory;
+
+
+public class DefaultTCIEnvironmentPerformance implements TCIEnvironmentPerformance
+{
+ public static final String ENV_SLOWNESS_FACTOR = "TCI_SLOWNESS_FACTOR";
+ public static final String PROPERTY_SLOWNESS_FACTOR = "tci.slowness.factor";
+
+ protected Integer slownessFactor;
+
+ /**
+ * Describes the performance of the underlying environment.
Higher values indicate a slower environment.
+ * Guideline is a follows:
+ *
+ * - 1 equals a standard developer machine CPU with roughly
16T 3GHz or better
+ * - for a Raspberry PI 5 with
4T 2.4GHz a value of roughly 3 should be chosen
+ *
+ * The default value is 1.
Min=1, Max=10
+ */
+ @Override
+ public int cpuSlownessFactor()
+ {
+ if(this.slownessFactor == null)
+ {
+ this.slownessFactor = this.parseSlownessFactor();
+ }
+ return this.slownessFactor;
+ }
+
+ protected synchronized int parseSlownessFactor()
+ {
+ return Optional.ofNullable(System.getenv(ENV_SLOWNESS_FACTOR))
+ .or(() -> Optional.ofNullable(System.getProperty(PROPERTY_SLOWNESS_FACTOR)))
+ .map(v -> {
+ try
+ {
+ return Math.min(Math.max(Integer.parseInt(v), 1), 10);
+ }
+ catch(final Exception e)
+ {
+ LoggerFactory.getLogger(this.getClass()).error("Unable to parse", e);
+ return null;
+ }
+ })
+ .orElse(1);
+ }
+}
diff --git a/base/src/main/java/software/xdev/tci/envperf/impl/TCIEnvironmentPerformance.java b/base/src/main/java/software/xdev/tci/envperf/impl/TCIEnvironmentPerformance.java
new file mode 100644
index 00000000..110aff5c
--- /dev/null
+++ b/base/src/main/java/software/xdev/tci/envperf/impl/TCIEnvironmentPerformance.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2024 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.envperf.impl;
+
+public interface TCIEnvironmentPerformance
+{
+ /**
+ * Describes the performance of the underlying environment.
Higher values indicate a slower environment.
+ * Guideline is a follows:
+ *
+ * - 1 equals a standard developer machine CPU with roughly
16T 3GHz or better
+ * - for a Raspberry PI 5 with
4T 2.4GHz a value of roughly 3 should be chosen
+ *
+ * The default value is 1.
Min=1, Max=10
+ */
+ int cpuSlownessFactor();
+}
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/BaseTCIFactory.java b/base/src/main/java/software/xdev/tci/factory/BaseTCIFactory.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/BaseTCIFactory.java
rename to base/src/main/java/software/xdev/tci/factory/BaseTCIFactory.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/TCIFactory.java b/base/src/main/java/software/xdev/tci/factory/TCIFactory.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/TCIFactory.java
rename to base/src/main/java/software/xdev/tci/factory/TCIFactory.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/ondemand/OnDemandTCIFactory.java b/base/src/main/java/software/xdev/tci/factory/ondemand/OnDemandTCIFactory.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/ondemand/OnDemandTCIFactory.java
rename to base/src/main/java/software/xdev/tci/factory/ondemand/OnDemandTCIFactory.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java b/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java
similarity index 97%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java
index 16208e3a..eb7e8201 100644
--- a/tci-base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java
+++ b/base/src/main/java/software/xdev/tci/factory/prestart/PreStartableTCIFactory.java
@@ -51,7 +51,7 @@
* What is PreStarting?
*
* When running tests usually there are certain times when the available resources are barely utilized:
- *
+ *
*
*
* PreStarting uses a "cached" pool of infrastructure and tries to utilize these idle times to fill/replenish this
@@ -483,22 +483,26 @@ public void close()
GlobalPreStartCoordinator.instance().unregister(this);
}
this.executorService.shutdown();
- final List> stopCFs = this.preStartQueue.stream()
- .map(i -> CompletableFuture.runAsync(() ->
- {
- try
- {
- i.infra().stop();
- }
- catch(final Exception e)
+
+ if(this.preStartQueue != null)
+ {
+ final List> stopCFs = this.preStartQueue.stream()
+ .map(i -> CompletableFuture.runAsync(() ->
{
- this.log().warn("[{}] Failed to shutdown infra", this.name, e);
- }
- }))
- .toList();
- stopCFs.forEach(CompletableFuture::join);
- // De-Ref for GC
- this.preStartQueue.clear();
+ try
+ {
+ i.infra().stop();
+ }
+ catch(final Exception e)
+ {
+ this.log().warn("[{}] Failed to shutdown infra", this.name, e);
+ }
+ }))
+ .toList();
+ stopCFs.forEach(CompletableFuture::join);
+ // De-Ref for GC
+ this.preStartQueue.clear();
+ }
super.close();
}
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java b/base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/config/DefaultPreStartConfig.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/config/PreStartConfig.java b/base/src/main/java/software/xdev/tci/factory/prestart/config/PreStartConfig.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/config/PreStartConfig.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/config/PreStartConfig.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/coordinator/DefaultGlobalPreStartCoordinator.java b/base/src/main/java/software/xdev/tci/factory/prestart/coordinator/DefaultGlobalPreStartCoordinator.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/coordinator/DefaultGlobalPreStartCoordinator.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/coordinator/DefaultGlobalPreStartCoordinator.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/coordinator/GlobalPreStartCoordinator.java b/base/src/main/java/software/xdev/tci/factory/prestart/coordinator/GlobalPreStartCoordinator.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/coordinator/GlobalPreStartCoordinator.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/coordinator/GlobalPreStartCoordinator.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/coordinator/endingdetector/PreStartTestEndingDetector.java b/base/src/main/java/software/xdev/tci/factory/prestart/coordinator/endingdetector/PreStartTestEndingDetector.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/coordinator/endingdetector/PreStartTestEndingDetector.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/coordinator/endingdetector/PreStartTestEndingDetector.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/DefaultDockerLoadMonitor.java b/base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/DefaultDockerLoadMonitor.java
similarity index 92%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/DefaultDockerLoadMonitor.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/DefaultDockerLoadMonitor.java
index fc5d960a..4bdef053 100644
--- a/tci-base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/DefaultDockerLoadMonitor.java
+++ b/base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/DefaultDockerLoadMonitor.java
@@ -15,8 +15,6 @@
*/
package software.xdev.tci.factory.prestart.loadbalancing;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -36,6 +34,7 @@
import com.github.dockerjava.zerodep.shaded.org.apache.hc.core5.http.HttpStatus;
+import software.xdev.tci.misc.http.HttpClientCloser;
import software.xdev.tci.safestart.SafeNamedContainerStarter;
@@ -168,16 +167,7 @@ public void close()
this.scrapeExecutor.shutdown();
}
- // Shutdown is only supported in Java 21+
- try
- {
- final Method mClose = HttpClient.class.getDeclaredMethod("close");
- mClose.invoke(this.httpClient);
- }
- catch(final NoSuchMethodException | IllegalAccessException | InvocationTargetException ex)
- {
- LOG.debug("Unable to close HttpClient. Likely running on Java < 21 where method does not exist", ex);
- }
+ HttpClientCloser.close(this.httpClient);
this.nodeExporterContainer.stop();
}
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/LoadMonitor.java b/base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/LoadMonitor.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/LoadMonitor.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/LoadMonitor.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/NodeExporterContainer.java b/base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/NodeExporterContainer.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/NodeExporterContainer.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/loadbalancing/NodeExporterContainer.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/CommitedImageSnapshotManager.java b/base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/CommitedImageSnapshotManager.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/CommitedImageSnapshotManager.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/CommitedImageSnapshotManager.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/SetImageIntoContainer.java b/base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/SetImageIntoContainer.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/SetImageIntoContainer.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/SetImageIntoContainer.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/SnapshotManager.java b/base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/SnapshotManager.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/SnapshotManager.java
rename to base/src/main/java/software/xdev/tci/factory/prestart/snapshoting/SnapshotManager.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/registry/DefaultTCIFactoryRegistry.java b/base/src/main/java/software/xdev/tci/factory/registry/DefaultTCIFactoryRegistry.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/registry/DefaultTCIFactoryRegistry.java
rename to base/src/main/java/software/xdev/tci/factory/registry/DefaultTCIFactoryRegistry.java
diff --git a/tci-base/src/main/java/software/xdev/tci/factory/registry/TCIFactoryRegistry.java b/base/src/main/java/software/xdev/tci/factory/registry/TCIFactoryRegistry.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/factory/registry/TCIFactoryRegistry.java
rename to base/src/main/java/software/xdev/tci/factory/registry/TCIFactoryRegistry.java
diff --git a/tci-base/src/main/java/software/xdev/tci/leakdetection/LeakDetectionAsyncReaper.java b/base/src/main/java/software/xdev/tci/leakdetection/LeakDetectionAsyncReaper.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/leakdetection/LeakDetectionAsyncReaper.java
rename to base/src/main/java/software/xdev/tci/leakdetection/LeakDetectionAsyncReaper.java
diff --git a/tci-base/src/main/java/software/xdev/tci/leakdetection/TCILeakAgent.java b/base/src/main/java/software/xdev/tci/leakdetection/TCILeakAgent.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/leakdetection/TCILeakAgent.java
rename to base/src/main/java/software/xdev/tci/leakdetection/TCILeakAgent.java
diff --git a/tci-base/src/main/java/software/xdev/tci/leakdetection/config/DefaultLeakDetectionConfig.java b/base/src/main/java/software/xdev/tci/leakdetection/config/DefaultLeakDetectionConfig.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/leakdetection/config/DefaultLeakDetectionConfig.java
rename to base/src/main/java/software/xdev/tci/leakdetection/config/DefaultLeakDetectionConfig.java
diff --git a/tci-base/src/main/java/software/xdev/tci/leakdetection/config/LeakDetectionConfig.java b/base/src/main/java/software/xdev/tci/leakdetection/config/LeakDetectionConfig.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/leakdetection/config/LeakDetectionConfig.java
rename to base/src/main/java/software/xdev/tci/leakdetection/config/LeakDetectionConfig.java
diff --git a/tci-base/src/main/java/software/xdev/tci/misc/ContainerMemory.java b/base/src/main/java/software/xdev/tci/misc/ContainerMemory.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/misc/ContainerMemory.java
rename to base/src/main/java/software/xdev/tci/misc/ContainerMemory.java
diff --git a/base/src/main/java/software/xdev/tci/misc/http/HttpClientCloser.java b/base/src/main/java/software/xdev/tci/misc/http/HttpClientCloser.java
new file mode 100644
index 00000000..965cd65e
--- /dev/null
+++ b/base/src/main/java/software/xdev/tci/misc/http/HttpClientCloser.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright © 2024 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.misc.http;
+
+import java.net.http.HttpClient;
+
+
+public final class HttpClientCloser
+{
+ @SuppressWarnings("unused")
+ public static void close(final HttpClient httpClient)
+ {
+ // Java 17 and below: Do nothing because close method does not exist
+ }
+
+ private HttpClientCloser()
+ {
+ }
+}
diff --git a/tci-base/src/main/java/software/xdev/tci/network/LazyNetwork.java b/base/src/main/java/software/xdev/tci/network/LazyNetwork.java
similarity index 99%
rename from tci-base/src/main/java/software/xdev/tci/network/LazyNetwork.java
rename to base/src/main/java/software/xdev/tci/network/LazyNetwork.java
index 8fbc7689..e694ea15 100644
--- a/tci-base/src/main/java/software/xdev/tci/network/LazyNetwork.java
+++ b/base/src/main/java/software/xdev/tci/network/LazyNetwork.java
@@ -315,6 +315,7 @@ public int getDeleteNetworkOnCloseTries()
*/
@Deprecated(forRemoval = true)
@Override
+ @SuppressWarnings("java:S1133")
public Statement apply(final Statement base, final Description description)
{
return null;
diff --git a/tci-base/src/main/java/software/xdev/tci/network/LazyNetworkPool.java b/base/src/main/java/software/xdev/tci/network/LazyNetworkPool.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/network/LazyNetworkPool.java
rename to base/src/main/java/software/xdev/tci/network/LazyNetworkPool.java
diff --git a/tci-base/src/main/java/software/xdev/tci/portfixation/AdditionalPortsForFixedExposingContainer.java b/base/src/main/java/software/xdev/tci/portfixation/AdditionalPortsForFixedExposingContainer.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/portfixation/AdditionalPortsForFixedExposingContainer.java
rename to base/src/main/java/software/xdev/tci/portfixation/AdditionalPortsForFixedExposingContainer.java
diff --git a/tci-base/src/main/java/software/xdev/tci/portfixation/PortFixation.java b/base/src/main/java/software/xdev/tci/portfixation/PortFixation.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/portfixation/PortFixation.java
rename to base/src/main/java/software/xdev/tci/portfixation/PortFixation.java
diff --git a/tci-base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java b/base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java
rename to base/src/main/java/software/xdev/tci/safestart/SafeNamedContainerStarter.java
diff --git a/tci-base/src/main/java/software/xdev/tci/serviceloading/TCIProviderPriority.java b/base/src/main/java/software/xdev/tci/serviceloading/TCIProviderPriority.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/serviceloading/TCIProviderPriority.java
rename to base/src/main/java/software/xdev/tci/serviceloading/TCIProviderPriority.java
diff --git a/tci-base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java b/base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java
rename to base/src/main/java/software/xdev/tci/serviceloading/TCIServiceLoader.java
diff --git a/tci-base/src/main/java/software/xdev/tci/tracing/TCITracer.java b/base/src/main/java/software/xdev/tci/tracing/TCITracer.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/tracing/TCITracer.java
rename to base/src/main/java/software/xdev/tci/tracing/TCITracer.java
diff --git a/tci-base/src/main/java/software/xdev/tci/tracing/TCITracingAgent.java b/base/src/main/java/software/xdev/tci/tracing/TCITracingAgent.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/tracing/TCITracingAgent.java
rename to base/src/main/java/software/xdev/tci/tracing/TCITracingAgent.java
diff --git a/tci-base/src/main/java/software/xdev/tci/tracing/config/DefaultTracingConfig.java b/base/src/main/java/software/xdev/tci/tracing/config/DefaultTracingConfig.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/tracing/config/DefaultTracingConfig.java
rename to base/src/main/java/software/xdev/tci/tracing/config/DefaultTracingConfig.java
diff --git a/tci-base/src/main/java/software/xdev/tci/tracing/config/TracingConfig.java b/base/src/main/java/software/xdev/tci/tracing/config/TracingConfig.java
similarity index 100%
rename from tci-base/src/main/java/software/xdev/tci/tracing/config/TracingConfig.java
rename to base/src/main/java/software/xdev/tci/tracing/config/TracingConfig.java
diff --git a/base/src/main/java21/software/xdev/tci/misc/http/HttpClientCloser.java b/base/src/main/java21/software/xdev/tci/misc/http/HttpClientCloser.java
new file mode 100644
index 00000000..2e9b6883
--- /dev/null
+++ b/base/src/main/java21/software/xdev/tci/misc/http/HttpClientCloser.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright © 2024 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.misc.http;
+
+import java.net.http.HttpClient;
+
+
+public final class HttpClientCloser
+{
+ @SuppressWarnings("unused")
+ public static void close(final HttpClient httpClient)
+ {
+ // Java 21+: close it
+ httpClient.close();
+ }
+
+ private HttpClientCloser()
+ {
+ }
+}
diff --git a/tci-base/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/base/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
similarity index 100%
rename from tci-base/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
rename to base/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
diff --git a/base/src/main/resources/META-INF/services/software.xdev.tci.envperf.impl.TCIEnvironmentPerformance b/base/src/main/resources/META-INF/services/software.xdev.tci.envperf.impl.TCIEnvironmentPerformance
new file mode 100644
index 00000000..df079c0c
--- /dev/null
+++ b/base/src/main/resources/META-INF/services/software.xdev.tci.envperf.impl.TCIEnvironmentPerformance
@@ -0,0 +1 @@
+software.xdev.tci.envperf.impl.DefaultTCIEnvironmentPerformance
diff --git a/tci-base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.config.PreStartConfig b/base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.config.PreStartConfig
similarity index 100%
rename from tci-base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.config.PreStartConfig
rename to base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.config.PreStartConfig
diff --git a/tci-base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.coordinator.GlobalPreStartCoordinator b/base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.coordinator.GlobalPreStartCoordinator
similarity index 100%
rename from tci-base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.coordinator.GlobalPreStartCoordinator
rename to base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.coordinator.GlobalPreStartCoordinator
diff --git a/tci-base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.loadbalancing.LoadMonitor b/base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.loadbalancing.LoadMonitor
similarity index 100%
rename from tci-base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.loadbalancing.LoadMonitor
rename to base/src/main/resources/META-INF/services/software.xdev.tci.factory.prestart.loadbalancing.LoadMonitor
diff --git a/tci-base/src/main/resources/META-INF/services/software.xdev.tci.factory.registry.TCIFactoryRegistry b/base/src/main/resources/META-INF/services/software.xdev.tci.factory.registry.TCIFactoryRegistry
similarity index 100%
rename from tci-base/src/main/resources/META-INF/services/software.xdev.tci.factory.registry.TCIFactoryRegistry
rename to base/src/main/resources/META-INF/services/software.xdev.tci.factory.registry.TCIFactoryRegistry
diff --git a/tci-base/src/main/resources/META-INF/services/software.xdev.tci.leakdetection.config.LeakDetectionConfig b/base/src/main/resources/META-INF/services/software.xdev.tci.leakdetection.config.LeakDetectionConfig
similarity index 100%
rename from tci-base/src/main/resources/META-INF/services/software.xdev.tci.leakdetection.config.LeakDetectionConfig
rename to base/src/main/resources/META-INF/services/software.xdev.tci.leakdetection.config.LeakDetectionConfig
diff --git a/tci-base/src/main/resources/META-INF/services/software.xdev.tci.tracing.config.TracingConfig b/base/src/main/resources/META-INF/services/software.xdev.tci.tracing.config.TracingConfig
similarity index 100%
rename from tci-base/src/main/resources/META-INF/services/software.xdev.tci.tracing.config.TracingConfig
rename to base/src/main/resources/META-INF/services/software.xdev.tci.tracing.config.TracingConfig
diff --git a/tci-base/src/test/java/software/xdev/tci/commitedimage/SetImageIntoContainerTest.java b/base/src/test/java/software/xdev/tci/commitedimage/SetImageIntoContainerTest.java
similarity index 100%
rename from tci-base/src/test/java/software/xdev/tci/commitedimage/SetImageIntoContainerTest.java
rename to base/src/test/java/software/xdev/tci/commitedimage/SetImageIntoContainerTest.java
diff --git a/tci-base/src/test/java/software/xdev/tci/portfixation/PortFixationTest.java b/base/src/test/java/software/xdev/tci/portfixation/PortFixationTest.java
similarity index 100%
rename from tci-base/src/test/java/software/xdev/tci/portfixation/PortFixationTest.java
rename to base/src/test/java/software/xdev/tci/portfixation/PortFixationTest.java
diff --git a/bom/README.md b/bom/README.md
new file mode 100644
index 00000000..9d41cdb8
--- /dev/null
+++ b/bom/README.md
@@ -0,0 +1,16 @@
+# BOM - [Bill of Materials](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Bill_of_Materials_.28BOM.29_POMs)
+
+Add it like this
+```xml
+
+
+
+ software.xdev.tci
+ bom
+ ...
+ pom
+ import
+
+
+
+```
\ No newline at end of file
diff --git a/bom/pom.xml b/bom/pom.xml
new file mode 100644
index 00000000..d670921a
--- /dev/null
+++ b/bom/pom.xml
@@ -0,0 +1,164 @@
+
+
+ 4.0.0
+
+ software.xdev.sse
+ bom
+ 2.0.0-SNAPSHOT
+ pom
+
+ bom
+ TCI - BOM
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ UTF-8
+ UTF-8
+
+
+
+
+
+ software.xdev.tci
+ base
+ 2.0.0-SNAPSHOT
+
+
+ software.xdev.tci
+ db-jdbc-orm
+ 2.0.0-SNAPSHOT
+
+
+ software.xdev.tci
+ jul-to-slf4j
+ 2.0.0-SNAPSHOT
+
+
+ software.xdev.tci
+ mockserver
+ 2.0.0-SNAPSHOT
+
+
+ software.xdev.tci
+ oidc-server-mock
+ 2.0.0-SNAPSHOT
+
+
+ software.xdev.tci
+ selenium
+ 2.0.0-SNAPSHOT
+
+
+ software.xdev.tci
+ spring-dao-support
+ 2.0.0-SNAPSHOT
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.1
+
+ bom
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+
diff --git a/db-jdbc-orm/README.md b/db-jdbc-orm/README.md
new file mode 100644
index 00000000..aa95430f
--- /dev/null
+++ b/db-jdbc-orm/README.md
@@ -0,0 +1,8 @@
+# DB JDBC ORM
+
+Common TCI code for Database info.
+
+## Features
+* Improved JDBC Container Waiting Strategy
+* Datageneration Template
+* Predefined support for creation of EntityManager and Entity discovery
diff --git a/db-jdbc-orm/pom.xml b/db-jdbc-orm/pom.xml
new file mode 100644
index 00000000..5931e8d3
--- /dev/null
+++ b/db-jdbc-orm/pom.xml
@@ -0,0 +1,347 @@
+
+
+ 4.0.0
+
+ software.xdev.tci
+ db-jdbc-orm
+ 2.0.0-SNAPSHOT
+ jar
+
+ db-jdbc-orm
+ TCI - db-jdbc-orm
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 17
+ ${javaVersion}
+
+ UTF-8
+ UTF-8
+
+
+
+
+ software.xdev.tci
+ base
+ 2.0.0-SNAPSHOT
+
+
+
+ org.testcontainers
+ jdbc
+ 1.21.3
+
+
+
+ jakarta.persistence
+ jakarta.persistence-api
+ 3.2.0
+
+
+
+ org.springframework
+ spring-core
+ 6.2.8
+
+
+ org.springframework
+ spring-orm
+ 6.2.8
+
+
+
+ org.hibernate.orm
+ hibernate-core
+ 6.6.19.Final
+
+
+ org.hibernate.orm
+ hibernate-hikaricp
+ 6.6.19.Final
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.13.2
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.17
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 5.0.0
+
+
+ ${project.organization.url}
+
+
+
+ com/mycila/maven/plugin/license/templates/APACHE-2.txt
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+ first
+
+ format
+
+ process-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ ${maven.compiler.release}
+
+ -proc:none
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.2
+
+
+ attach-javadocs
+ package
+
+ jar
+
+
+
+
+ true
+ none
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.3
+
+
+
+
+
+ ignore-service-loading
+
+
+
+ src/main/resources
+
+ META-INF/services/**
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.1
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+ checkstyle
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.26.1
+
+
+
+ ../.config/checkstyle/checkstyle.xml
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+ pmd
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.27.0
+
+ true
+ true
+
+ ../.config/pmd/java/ruleset.xml
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+ 7.15.0
+
+
+ net.sourceforge.pmd
+ pmd-java
+ 7.15.0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 3.6.0
+
+
+
+
+
+
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/BaseDBTCI.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/BaseDBTCI.java
new file mode 100644
index 00000000..2cefad40
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/BaseDBTCI.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db;
+
+import java.sql.Driver;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import javax.sql.DataSource;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.EntityManagerFactory;
+
+import org.hibernate.cfg.PersistenceSettings;
+import org.hibernate.hikaricp.internal.HikariCPConnectionProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+
+import software.xdev.tci.TCI;
+import software.xdev.tci.db.persistence.EntityManagerController;
+import software.xdev.tci.db.persistence.EntityManagerControllerFactory;
+import software.xdev.tci.db.persistence.hibernate.CachingStandardScanner;
+
+
+public abstract class BaseDBTCI> extends TCI
+{
+ protected static final Map, Logger> LOGGER_CACHE = Collections.synchronizedMap(new WeakHashMap<>());
+
+ public static final String DEFAULT_DATABASE = "testdb";
+ public static final String DEFAULT_USERNAME = "testuser";
+ @SuppressWarnings("java:S2068") // This is only for tests
+ public static final String DEFAULT_PASSWORD = "testpw";
+
+ protected final boolean migrateAndInitializeEMC;
+ protected final Supplier emcFactorySupplier;
+ protected final Logger logger;
+
+ protected String database = DEFAULT_DATABASE;
+ protected String username = DEFAULT_USERNAME;
+ @SuppressWarnings("java:S2068") // This is only for tests
+ protected String password = DEFAULT_PASSWORD;
+
+ protected EntityManagerController emc;
+
+ protected BaseDBTCI(
+ final C container,
+ final String networkAlias,
+ final boolean migrateAndInitializeEMC,
+ final Supplier emcFactorySupplier)
+ {
+ super(container, networkAlias);
+ this.migrateAndInitializeEMC = migrateAndInitializeEMC;
+ this.emcFactorySupplier = emcFactorySupplier;
+
+ this.logger = LOGGER_CACHE.computeIfAbsent(this.getClass(), LoggerFactory::getLogger);
+ }
+
+ @Override
+ public void start(final String containerName)
+ {
+ super.start(containerName);
+ if(this.migrateAndInitializeEMC)
+ {
+ // Do basic migrations async
+ this.log().debug("Running migration to basic structure");
+ this.execInitialDatabaseMigration();
+ this.log().info("Migration executed");
+
+ // Create EMC in background to improve performance (~5s)
+ this.log().debug("Initializing EntityManagerController...");
+ this.getEMC();
+ this.log().info("Initialized EntityManagerController");
+ }
+ }
+
+ @Override
+ public void stop()
+ {
+ if(this.emc != null)
+ {
+ try
+ {
+ this.emc.close();
+ }
+ catch(final Exception ex)
+ {
+ this.log().warn("Failed to close EntityManagerController", ex);
+ }
+ this.emc = null;
+ }
+ super.stop();
+ }
+
+ public EntityManagerController getEMC()
+ {
+ if(this.emc == null)
+ {
+ this.initEMCIfRequired();
+ }
+
+ return this.emc;
+ }
+
+ protected abstract Class extends Driver> driverClazz();
+
+ protected synchronized void initEMCIfRequired()
+ {
+ if(this.emc != null)
+ {
+ return;
+ }
+
+ final EntityManagerControllerFactory emcFactory = this.emcFactorySupplier.get();
+ this.emc = emcFactory
+ .withDriverFullClassName(this.driverClazz().getName())
+ // Use production-ready pool; otherwise Hibernate warnings occur
+ .withConnectionProviderClassName(HikariCPConnectionProvider.class.getName())
+ .withJdbcUrl(this.getExternalJDBCUrl())
+ .withUsername(this.username)
+ .withPassword(this.password)
+ .withAdditionalConfig(Map.ofEntries(
+ // Use caching scanner to massively improve performance (this way the scanning only happens once)
+ Map.entry(PersistenceSettings.SCANNER, CachingStandardScanner.instance())
+ ))
+ .build();
+ }
+
+ public String getExternalJDBCUrl()
+ {
+ return this.getContainer().getJdbcUrl();
+ }
+
+ /**
+ * Creates a new {@link EntityManager} with an internal {@link EntityManagerFactory}, which can be used to load and
+ * save data in the database for the test.
+ *
+ *
+ * It may be a good idea to close the EntityManager, when you're finished with it.
+ *
+ *
+ * All created EntityManager are automatically cleaned up when the test is finished.
+ *
+ *
+ * @return EntityManager
+ */
+ public EntityManager createEntityManager()
+ {
+ return this.getEMC().createEntityManager();
+ }
+
+ public void useNewEntityManager(final Consumer action)
+ {
+ try(final EntityManager em = this.createEntityManager())
+ {
+ action.accept(em);
+ }
+ }
+
+ @SuppressWarnings("java:S6437") // Only done for test
+ public abstract DataSource createDataSource();
+
+ protected abstract void execInitialDatabaseMigration();
+
+ public void migrateDatabase(final Collection locations)
+ {
+ this.migrateDatabase(locations.toArray(String[]::new));
+ }
+
+ public abstract void migrateDatabase(final String... locations);
+
+ protected Logger log()
+ {
+ return this.logger;
+ }
+
+ // region Configure
+
+ public BaseDBTCI withDatabase(final String database)
+ {
+ this.database = database;
+ return this;
+ }
+
+ public BaseDBTCI withUsername(final String username)
+ {
+ this.username = username;
+ return this;
+ }
+
+ public BaseDBTCI withPassword(final String password)
+ {
+ this.password = password;
+ return this;
+ }
+
+ public boolean isMigrateAndInitializeEMC()
+ {
+ return this.migrateAndInitializeEMC;
+ }
+
+ // endregion
+}
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/containers/TestQueryStringAccessor.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/containers/TestQueryStringAccessor.java
new file mode 100644
index 00000000..1d005f9e
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/containers/TestQueryStringAccessor.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.containers;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.testcontainers.containers.JdbcDatabaseContainer;
+
+
+public final class TestQueryStringAccessor
+{
+ @SuppressWarnings("java:S3011")
+ public static String testQueryString(final JdbcDatabaseContainer> container)
+ throws InvocationTargetException, IllegalAccessException
+ {
+ Class> currentClass = container.getClass();
+ while(currentClass != null && !JdbcDatabaseContainer.class.equals(currentClass))
+ {
+ try
+ {
+ final Method mGetTestQueryString = currentClass.getDeclaredMethod("getTestQueryString");
+ mGetTestQueryString.setAccessible(true);
+ return (String)mGetTestQueryString.invoke(container);
+ }
+ catch(final NoSuchMethodException ignored)
+ {
+ // Skip
+ }
+ currentClass = currentClass.getSuperclass();
+ }
+ return null;
+ }
+
+ private TestQueryStringAccessor()
+ {
+ }
+}
diff --git a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/WaitableJDBCContainer.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java
similarity index 51%
rename from tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/WaitableJDBCContainer.java
rename to db-jdbc-orm/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java
index c0c456bc..432dd0ba 100644
--- a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/containers/WaitableJDBCContainer.java
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java
@@ -1,7 +1,21 @@
-package software.xdev.tci.demo.tci.db.containers;
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.containers;
import java.sql.Connection;
-import java.sql.Statement;
import java.util.concurrent.TimeUnit;
import org.rnorth.ducttape.TimeoutException;
@@ -16,14 +30,12 @@
import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;
-interface WaitableJDBCContainer extends WaitStrategyTarget
+public interface WaitableJDBCContainer extends WaitStrategyTarget
{
default WaitStrategy completeJDBCWaitStrategy()
{
return new WaitAllStrategy()
- // First wait for ports to be accessible
.withStrategy(Wait.defaultWaitStrategy())
- // then check if a JDBC connection (requires more resources) is possible
.withStrategy(new JDBCWaitStrategy());
}
@@ -38,8 +50,6 @@ default void waitUntilContainerStarted()
}
}
- String getTestQueryString();
-
/**
* @apiNote Assumes that the container is already started
*/
@@ -58,8 +68,7 @@ public JDBCWaitStrategy()
@Override
protected void waitUntilReady()
{
- if(!(this.waitStrategyTarget instanceof final JdbcDatabaseContainer> container
- && this.waitStrategyTarget instanceof final WaitableJDBCContainer waitableJDBCContainer))
+ if(!(this.waitStrategyTarget instanceof final JdbcDatabaseContainer> container))
{
throw new IllegalArgumentException(
"Container must implement JdbcDatabaseContainer and WaitableJDBCContainer");
@@ -67,17 +76,7 @@ protected void waitUntilReady()
try
{
- Unreliables.retryUntilTrue(
- (int)this.startupTimeout.getSeconds(),
- TimeUnit.SECONDS,
- () -> this.getRateLimiter().getWhenReady(() -> {
- try(final Connection connection = container.createConnection("");
- final Statement statement = connection.createStatement())
- {
- return statement.execute(waitableJDBCContainer.getTestQueryString());
- }
- })
- );
+ this.waitUntilJDBCValid(container);
}
catch(final TimeoutException e)
{
@@ -87,5 +86,37 @@ protected void waitUntilReady()
+ "), please check container logs");
}
}
+
+ protected void waitUntilJDBCValid(final JdbcDatabaseContainer> container)
+ {
+ Unreliables.retryUntilTrue(
+ (int)this.startupTimeout.getSeconds(),
+ TimeUnit.SECONDS,
+ // Rate limit creation of connections as this is quite an expensive operation
+ () -> this.getRateLimiter().getWhenReady(() -> {
+ try(final Connection connection = container.createConnection(""))
+ {
+ return this.waitUntilJDBCConnectionValidated(container, connection);
+ }
+ })
+ );
+ }
+
+ @SuppressWarnings("unused") // Variable might be used by extension
+ protected boolean waitUntilJDBCConnectionValidated(
+ final JdbcDatabaseContainer> container,
+ final Connection connection)
+ {
+ return Unreliables.retryUntilSuccess(
+ (int)this.startupTimeout.getSeconds(),
+ TimeUnit.SECONDS,
+ () -> this.validateJDBCConnection(connection));
+ }
+
+ @SuppressWarnings("java:S112")
+ protected boolean validateJDBCConnection(final Connection connection) throws Exception
+ {
+ return connection.isValid(10);
+ }
}
}
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/datageneration/BaseDBDataGenerator.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/datageneration/BaseDBDataGenerator.java
new file mode 100644
index 00000000..2b57d6dc
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/datageneration/BaseDBDataGenerator.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.datageneration;
+
+import java.time.LocalDate;
+import java.util.Objects;
+
+import jakarta.persistence.EntityManager;
+
+import software.xdev.tci.db.persistence.TransactionExecutor;
+
+
+/**
+ * Base class for all data generators. Holds an {@link EntityManager} and a {@link TransactionExecutor} to save data.
+ *
+ * @implNote Due to generics save Methods need to be implemented downstream
+ */
+public abstract class BaseDBDataGenerator implements DataGenerator
+{
+ protected final EntityManager em;
+ protected final TransactionExecutor transactor;
+
+ protected BaseDBDataGenerator(final EntityManager em)
+ {
+ this(em, null);
+ }
+
+ protected BaseDBDataGenerator(final EntityManager em, final TransactionExecutor transactor)
+ {
+ this.em = Objects.requireNonNull(em, "EntityManager can't be null!");
+ this.transactor = transactor != null ? transactor : new TransactionExecutor(em);
+ }
+
+ /**
+ * Returns the {@link EntityManager}-Instance of this generator, which can be used to save data.
+ */
+ @SuppressWarnings("java:S1845") // Record style access
+ protected EntityManager em()
+ {
+ return this.em;
+ }
+
+ /**
+ * Returns the {@link TransactionExecutor}-Instance of this generator, which can be used to save data with a
+ * transaction.
+ */
+ @SuppressWarnings("java:S1845") // Record style access
+ protected TransactionExecutor transactor()
+ {
+ return this.transactor;
+ }
+
+ /**
+ * Returns a {@link LocalDate} in the past. By default, 1970-01-01.
+ */
+ @SuppressWarnings("checkstyle:MagicNumber")
+ public LocalDate getLocalDateInPast()
+ {
+ return LocalDate.of(1970, 1, 1);
+ }
+
+ /**
+ * Returns a {@link LocalDate} in the future. By default, 3000-01-01.
+ */
+ @SuppressWarnings("checkstyle:MagicNumber")
+ public LocalDate getLocalDateInFuture()
+ {
+ return LocalDate.of(3000, 1, 1).plusYears(1);
+ }
+}
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/datageneration/DataGenerator.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/datageneration/DataGenerator.java
new file mode 100644
index 00000000..7326fd26
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/datageneration/DataGenerator.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.datageneration;
+
+public interface DataGenerator
+{
+ // just marker
+}
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/factory/BaseDBTCIFactory.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/factory/BaseDBTCIFactory.java
new file mode 100644
index 00000000..549c1569
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/factory/BaseDBTCIFactory.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.factory;
+
+import java.sql.Connection;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+import org.rnorth.ducttape.unreliables.Unreliables;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+
+import software.xdev.tci.db.BaseDBTCI;
+import software.xdev.tci.db.containers.TestQueryStringAccessor;
+import software.xdev.tci.envperf.EnvironmentPerformance;
+import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
+import software.xdev.tci.factory.prestart.config.PreStartConfig;
+
+
+public abstract class BaseDBTCIFactory, I extends BaseDBTCI>
+ extends PreStartableTCIFactory
+{
+ protected BaseDBTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder)
+ {
+ super(infraBuilder, containerBuilder, "db", "container.db", "DB");
+ }
+
+ protected BaseDBTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final PreStartConfig config,
+ final Timeouts timeouts)
+ {
+ super(infraBuilder, containerBuilder, "db", "container.db", "DB", config, timeouts);
+ }
+
+ protected BaseDBTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final String containerBaseName,
+ final String containerLoggerName,
+ final String name)
+ {
+ super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName, name);
+ }
+
+ protected BaseDBTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final String containerBaseName,
+ final String containerLoggerName,
+ final String name,
+ final PreStartConfig config,
+ final Timeouts timeouts)
+ {
+ super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName, name, config, timeouts);
+ }
+
+ @Override
+ protected void postProcessNew(final I infra)
+ {
+ // Docker needs a few milliseconds (usually less than 100) to reconfigure its networks
+ // In the meantime existing connections might fail if we proceed immediately
+ // So let's wait a moment here until everything is working
+ Unreliables.retryUntilSuccess(
+ 10 + EnvironmentPerformance.cpuSlownessFactor() * 2,
+ TimeUnit.SECONDS,
+ () -> {
+ try(final Connection con = infra.createDataSource().getConnection())
+ {
+ con.isValid(10);
+ }
+
+ if(infra.isMigrateAndInitializeEMC())
+ {
+ // Check EMC
+ infra.useNewEntityManager(em -> em
+ .createNativeQuery(Objects.requireNonNullElse(
+ this.getTestQueryStringForEntityManager(infra),
+ "SELECT 1"))
+ .getResultList());
+ }
+ return null;
+ });
+ }
+
+ protected String getTestQueryStringForEntityManager(final I infra)
+ {
+ try
+ {
+ return TestQueryStringAccessor.testQueryString(infra.getContainer());
+ }
+ catch(final Exception ex)
+ {
+ this.log().warn("Failed to get test query string", ex);
+ return null;
+ }
+ }
+}
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/EntityManagerController.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/EntityManagerController.java
new file mode 100644
index 00000000..58ded1c9
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/EntityManagerController.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.persistence;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.EntityManagerFactory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Handles the creation and destruction of {@link EntityManager}s.
+ *
+ * This should only be used when a {@link EntityManager} has to be created manually, e.g. during tests.
+ */
+public class EntityManagerController implements AutoCloseable
+{
+ private static final Logger LOG = LoggerFactory.getLogger(EntityManagerController.class);
+
+ protected final List activeEms = Collections.synchronizedList(new ArrayList<>());
+ protected final EntityManagerFactory emf;
+
+ public EntityManagerController(final EntityManagerFactory emf)
+ {
+ this.emf = Objects.requireNonNull(emf);
+ }
+
+ /**
+ * Creates a new {@link EntityManager} with an internal {@link EntityManagerFactory}, which can be used to load and
+ * save data in the database.
+ *
+ *
+ * It may be a good idea to close the EntityManager, when you're finished with it.
+ *
+ *
+ * All created EntityManager are automatically cleaned up once {@link #close()} is called.
+ *
+ *
+ * @return EntityManager
+ */
+ public EntityManager createEntityManager()
+ {
+ final EntityManager em = this.emf.createEntityManager();
+ this.activeEms.add(em);
+
+ return em;
+ }
+
+ @Override
+ public void close()
+ {
+ LOG.info("Shutting down resources");
+ this.activeEms.forEach(em ->
+ {
+ try
+ {
+ if(em.getTransaction() != null && em.getTransaction().isActive())
+ {
+ em.getTransaction().rollback();
+ }
+ em.close();
+ }
+ catch(final Exception e)
+ {
+ LOG.warn("Unable to close EntityManager", e);
+ }
+ });
+
+ LOG.info("Cleared {}x EntityManagers", this.activeEms.size());
+
+ this.activeEms.clear();
+
+ try
+ {
+ this.emf.close();
+ LOG.info("Released EntityManagerFactory");
+ }
+ catch(final Exception e)
+ {
+ LOG.error("Failed to release EntityManagerFactory", e);
+ }
+ }
+}
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/EntityManagerControllerFactory.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/EntityManagerControllerFactory.java
new file mode 100644
index 00000000..22912444
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/EntityManagerControllerFactory.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.persistence;
+
+import static java.util.Map.entry;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import jakarta.persistence.spi.ClassTransformer;
+import jakarta.persistence.spi.PersistenceUnitTransactionType;
+
+import org.hibernate.cfg.JdbcSettings;
+import org.hibernate.jpa.HibernatePersistenceProvider;
+import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
+
+import software.xdev.tci.db.persistence.hibernate.DisableHibernateFormatMapper;
+
+
+public class EntityManagerControllerFactory
+{
+ protected Supplier> entityClassesFinder;
+ protected String driverFullClassName;
+ protected String connectionProviderClassName;
+ protected String persistenceUnitName = "Test";
+ protected String jdbcUrl;
+ protected String username;
+ protected String password;
+ protected Map additionalConfig;
+
+ protected boolean disableHibernateFormatter = true;
+
+ public EntityManagerControllerFactory()
+ {
+ }
+
+ public EntityManagerControllerFactory(final Supplier> entityClassesFinder)
+ {
+ this.withEntityClassesFinder(entityClassesFinder);
+ }
+
+ public EntityManagerControllerFactory withEntityClassesFinder(final Supplier> entityClassesFinder)
+ {
+ this.entityClassesFinder = entityClassesFinder;
+ return this;
+ }
+
+ public EntityManagerControllerFactory withDriverFullClassName(final String driverFullClassName)
+ {
+ this.driverFullClassName = driverFullClassName;
+ return this;
+ }
+
+ public EntityManagerControllerFactory withConnectionProviderClassName(final String connectionProviderClassName)
+ {
+ this.connectionProviderClassName = connectionProviderClassName;
+ return this;
+ }
+
+ public EntityManagerControllerFactory withPersistenceUnitName(final String persistenceUnitName)
+ {
+ this.persistenceUnitName = persistenceUnitName;
+ return this;
+ }
+
+ public EntityManagerControllerFactory withJdbcUrl(final String jdbcUrl)
+ {
+ this.jdbcUrl = jdbcUrl;
+ return this;
+ }
+
+ public EntityManagerControllerFactory withUsername(final String username)
+ {
+ this.username = username;
+ return this;
+ }
+
+ public EntityManagerControllerFactory withPassword(final String password)
+ {
+ this.password = password;
+ return this;
+ }
+
+ public EntityManagerControllerFactory withAdditionalConfig(final Map additionalConfig)
+ {
+ this.additionalConfig = additionalConfig;
+ return this;
+ }
+
+ public EntityManagerControllerFactory withDisableHibernateFormatter(final boolean disableHibernateFormatter)
+ {
+ this.disableHibernateFormatter = disableHibernateFormatter;
+ return this;
+ }
+
+ protected MutablePersistenceUnitInfo createBasicMutablePersistenceUnitInfo()
+ {
+ final MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo()
+ {
+ @Override
+ public void addTransformer(final ClassTransformer classTransformer)
+ {
+ // Do nothing
+ }
+
+ @Override
+ public ClassLoader getNewTempClassLoader()
+ {
+ return null;
+ }
+ };
+ persistenceUnitInfo.setTransactionType(PersistenceUnitTransactionType.RESOURCE_LOCAL);
+ persistenceUnitInfo.setPersistenceUnitName(this.persistenceUnitName);
+ persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName());
+ return persistenceUnitInfo;
+ }
+
+ protected Collection jarFileUrlsToAdd() throws IOException
+ {
+ return Collections.list(EntityManagerController.class
+ .getClassLoader()
+ .getResources(""));
+ }
+
+ protected Map defaultPropertiesMap()
+ {
+ return new HashMap<>(Map.ofEntries(
+ entry(JdbcSettings.JAKARTA_JDBC_DRIVER, this.driverFullClassName),
+ entry(JdbcSettings.JAKARTA_JDBC_URL, this.jdbcUrl),
+ entry(JdbcSettings.JAKARTA_JDBC_USER, this.username),
+ entry(JdbcSettings.JAKARTA_JDBC_PASSWORD, this.password)
+ ));
+ }
+
+ public EntityManagerController build()
+ {
+ final MutablePersistenceUnitInfo persistenceUnitInfo = this.createBasicMutablePersistenceUnitInfo();
+ if(this.entityClassesFinder != null)
+ {
+ persistenceUnitInfo.getManagedClassNames().addAll(this.entityClassesFinder.get());
+ }
+ try
+ {
+ this.jarFileUrlsToAdd().forEach(persistenceUnitInfo::addJarFileUrl);
+ }
+ catch(final IOException ioe)
+ {
+ throw new UncheckedIOException(ioe);
+ }
+
+ final Map properties = this.defaultPropertiesMap();
+ Optional.ofNullable(this.connectionProviderClassName)
+ .ifPresent(p -> properties.put(JdbcSettings.CONNECTION_PROVIDER, this.connectionProviderClassName));
+ if(this.disableHibernateFormatter)
+ {
+ properties.putAll(DisableHibernateFormatMapper.properties());
+ }
+ properties.putAll(this.additionalConfig);
+ return new EntityManagerController(
+ new HibernatePersistenceProvider().createContainerEntityManagerFactory(
+ persistenceUnitInfo,
+ properties));
+ }
+}
diff --git a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/TransactionExecutor.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/TransactionExecutor.java
similarity index 60%
rename from tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/TransactionExecutor.java
rename to db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/TransactionExecutor.java
index 10866a5a..8fcba29d 100644
--- a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/TransactionExecutor.java
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/TransactionExecutor.java
@@ -1,4 +1,19 @@
-package software.xdev.tci.demo.tci.db.persistence;
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.persistence;
import java.util.Objects;
import java.util.function.Supplier;
diff --git a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/AnnotatedClassFinder.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/classfinder/AnnotatedClassFinder.java
similarity index 65%
rename from tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/AnnotatedClassFinder.java
rename to db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/classfinder/AnnotatedClassFinder.java
index fe34afbc..8a9c87f0 100644
--- a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/AnnotatedClassFinder.java
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/classfinder/AnnotatedClassFinder.java
@@ -1,4 +1,19 @@
-package software.xdev.tci.demo.tci.db.persistence;
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.persistence.classfinder;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -17,14 +32,10 @@
import org.springframework.util.SystemPropertyUtils;
-public final class AnnotatedClassFinder
+public class AnnotatedClassFinder
{
- private AnnotatedClassFinder()
- {
- }
-
@SuppressWarnings({"java:S1452", "java:S4968"}) // Returned so by stream
- public static List extends Class>> find(
+ public List extends Class>> find(
final String basePackage,
final Class extends Annotation> annotationClazz)
{
@@ -35,12 +46,14 @@ public static List extends Class>> find(
{
return Stream.of(resourcePatternResolver.getResources(
ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
- + resolveBasePackage(basePackage) + "/" + "**/*.class"))
+ + this.resolveBasePackage(basePackage) + "/" + "**/*.class"))
.filter(Resource::isReadable)
.map(resource -> {
try
{
- return getIfIsCandidate(metadataReaderFactory.getMetadataReader(resource), annotationClazz);
+ return this.getIfIsCandidate(
+ metadataReaderFactory.getMetadataReader(resource),
+ annotationClazz);
}
catch(final IOException e)
{
@@ -56,12 +69,12 @@ public static List extends Class>> find(
}
}
- private static String resolveBasePackage(final String basePackage)
+ protected String resolveBasePackage(final String basePackage)
{
return ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage));
}
- private static Class> getIfIsCandidate(
+ protected Class> getIfIsCandidate(
final MetadataReader metadataReader,
final Class extends Annotation> annotationClazz)
{
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/classfinder/CachedEntityAnnotatedClassNameFinder.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/classfinder/CachedEntityAnnotatedClassNameFinder.java
new file mode 100644
index 00000000..8ea09fc4
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/classfinder/CachedEntityAnnotatedClassNameFinder.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.persistence.classfinder;
+
+import java.lang.annotation.Annotation;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+
+public class CachedEntityAnnotatedClassNameFinder implements Supplier>
+{
+ protected final Supplier classFinderProvider = AnnotatedClassFinder::new;
+ protected final String basePackage;
+ protected final Class extends Annotation> annotationClazz;
+ protected Set cache;
+
+ public CachedEntityAnnotatedClassNameFinder(
+ final String basePackage,
+ final Class extends Annotation> annotationClazz)
+ {
+ this.basePackage = basePackage;
+ this.annotationClazz = annotationClazz;
+ }
+
+ @Override
+ public Set get()
+ {
+ if(this.cache == null)
+ {
+ this.cache = this.classFinderProvider.get()
+ .find(this.basePackage, this.annotationClazz)
+ .stream()
+ .map(Class::getName)
+ .collect(Collectors.toSet());
+ }
+ return this.cache;
+ }
+}
diff --git a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/hibernate/CachingStandardScanner.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/hibernate/CachingStandardScanner.java
similarity index 72%
rename from tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/hibernate/CachingStandardScanner.java
rename to db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/hibernate/CachingStandardScanner.java
index 3d6eb857..3b56f52b 100644
--- a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/hibernate/CachingStandardScanner.java
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/hibernate/CachingStandardScanner.java
@@ -1,4 +1,19 @@
-package software.xdev.tci.demo.tci.db.persistence.hibernate;
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.persistence.hibernate;
import java.net.URL;
import java.util.List;
@@ -12,6 +27,7 @@
import org.hibernate.boot.archive.scan.spi.ScanResult;
+@SuppressWarnings("java:S6548")
public class CachingStandardScanner extends StandardScanner
{
private static CachingStandardScanner instance;
diff --git a/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/hibernate/DisableHibernateFormatMapper.java b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/hibernate/DisableHibernateFormatMapper.java
new file mode 100644
index 00000000..638ae292
--- /dev/null
+++ b/db-jdbc-orm/src/main/java/software/xdev/tci/db/persistence/hibernate/DisableHibernateFormatMapper.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.persistence.hibernate;
+
+import java.util.Map;
+
+import org.hibernate.cfg.MappingSettings;
+import org.hibernate.type.descriptor.WrapperOptions;
+import org.hibernate.type.descriptor.java.JavaType;
+import org.hibernate.type.format.FormatMapper;
+
+
+/**
+ * Under normal circumstances Hibernate tries to automatically look up a formatMapper for JSON and XML.
+ *
+ * There are multiple problems with this:
+ *
+ * - It tries to use Jackson for XML which might not be configured -> CRASH
+ * - Storing XML, JSON or any other data structure inside a RELATIONAL DATABASE is idiotic
+ * - Lookup slows down boot
+ *
+ *
+ * @since Hibernate 6.3
+ */
+public final class DisableHibernateFormatMapper
+{
+ private DisableHibernateFormatMapper()
+ {
+ }
+
+ public static Map properties()
+ {
+ return Map.ofEntries(
+ Map.entry(MappingSettings.XML_MAPPING_ENABLED, false),
+ Map.entry(MappingSettings.JSON_FORMAT_MAPPER, new NoOpFormatMapper()),
+ Map.entry(MappingSettings.XML_FORMAT_MAPPER, new NoOpFormatMapper())
+ );
+ }
+
+ public static class NoOpFormatMapper implements FormatMapper
+ {
+ @Override
+ public T fromString(
+ final CharSequence charSequence,
+ final JavaType javaType,
+ final WrapperOptions wrapperOptions)
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String toString(final T value, final JavaType javaType, final WrapperOptions wrapperOptions)
+ {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/db-jdbc-orm/src/test/java/software/xdev/tci/db/containers/TestQueryStringAccessorTest.java b/db-jdbc-orm/src/test/java/software/xdev/tci/db/containers/TestQueryStringAccessorTest.java
new file mode 100644
index 00000000..2e97f4cb
--- /dev/null
+++ b/db-jdbc-orm/src/test/java/software/xdev/tci/db/containers/TestQueryStringAccessorTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.db.containers;
+
+import java.util.UUID;
+import java.util.concurrent.Future;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.images.RemoteDockerImage;
+import org.testcontainers.utility.DockerImageName;
+
+
+class TestQueryStringAccessorTest
+{
+ @ParameterizedTest
+ @MethodSource
+ void checkIfAccessorWorks(final String expected, final String provided) throws Exception
+ {
+ final MockJDBCContainer container = new MockJDBCContainer(
+ new RemoteDockerImage(DockerImageName.parse("test" + UUID.randomUUID()))
+ .withImagePullPolicy(ignored -> false), provided);
+ Assertions.assertEquals(expected, TestQueryStringAccessor.testQueryString(container));
+ }
+
+ static Stream checkIfAccessorWorks()
+ {
+ return Stream.of(
+ Arguments.of("SELECT 123", "SELECT 123"),
+ Arguments.of(null, null)
+ );
+ }
+
+ static class MockJDBCContainer extends JdbcDatabaseContainer
+ {
+ private final String testQueryString;
+
+ public MockJDBCContainer(final Future image, final String testQueryString)
+ {
+ super(image);
+ this.testQueryString = testQueryString;
+ }
+
+ @Override
+ public String getDriverClassName()
+ {
+ return "";
+ }
+
+ @Override
+ public String getJdbcUrl()
+ {
+ return "";
+ }
+
+ @Override
+ public String getUsername()
+ {
+ return "";
+ }
+
+ @Override
+ public String getPassword()
+ {
+ return "";
+ }
+
+ @Override
+ protected String getTestQueryString()
+ {
+ return this.testQueryString;
+ }
+ }
+}
diff --git a/jul-to-slf4j/README.md b/jul-to-slf4j/README.md
new file mode 100644
index 00000000..6f57b3ad
--- /dev/null
+++ b/jul-to-slf4j/README.md
@@ -0,0 +1,3 @@
+# JUL to SLF4J
+
+Logging Adapter to redirect JUL to SLF4J
diff --git a/jul-to-slf4j/pom.xml b/jul-to-slf4j/pom.xml
new file mode 100644
index 00000000..f77d173f
--- /dev/null
+++ b/jul-to-slf4j/pom.xml
@@ -0,0 +1,299 @@
+
+
+ 4.0.0
+
+ software.xdev.tci
+ jul-to-slf4j
+ 2.0.0-SNAPSHOT
+ jar
+
+ jul-to-slf4j
+ TCI - jul-to-slf4j
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 17
+ ${javaVersion}
+
+ UTF-8
+ UTF-8
+
+
+
+
+ org.slf4j
+ jul-to-slf4j
+ 2.0.17
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 5.0.0
+
+
+ ${project.organization.url}
+
+
+
+ com/mycila/maven/plugin/license/templates/APACHE-2.txt
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+ first
+
+ format
+
+ process-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ ${maven.compiler.release}
+
+ -proc:none
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.2
+
+
+ attach-javadocs
+ package
+
+ jar
+
+
+
+
+ true
+ none
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.3
+
+
+
+
+
+ ignore-service-loading
+
+
+
+ src/main/resources
+
+ META-INF/services/**
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.1
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+ checkstyle
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.26.1
+
+
+
+ ../.config/checkstyle/checkstyle.xml
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+ pmd
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.27.0
+
+ true
+ true
+
+ ../.config/pmd/java/ruleset.xml
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+ 7.15.0
+
+
+ net.sourceforge.pmd
+ pmd-java
+ 7.15.0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 3.6.0
+
+
+
+
+
+
diff --git a/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java b/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java
new file mode 100644
index 00000000..be2d0675
--- /dev/null
+++ b/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.logging;
+
+import org.slf4j.LoggerFactory;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+
+public class JULtoSLF4JRedirector
+{
+ static final JULtoSLF4JRedirector INSTANCE = new JULtoSLF4JRedirector();
+
+ protected boolean installed;
+
+ protected JULtoSLF4JRedirector()
+ {
+ }
+
+ protected void redirectInternal()
+ {
+ if(this.installed)
+ {
+ return;
+ }
+ if(SLF4JBridgeHandler.isInstalled())
+ {
+ this.installed = true;
+ return;
+ }
+
+ LoggerFactory.getLogger(JULtoSLF4JRedirector.class).debug("Installing SLF4JBridgeHandler");
+ SLF4JBridgeHandler.removeHandlersForRootLogger();
+ SLF4JBridgeHandler.install();
+ this.installed = true;
+ }
+
+ public static void redirect()
+ {
+ INSTANCE.redirectInternal();
+ }
+}
diff --git a/mockserver/README.md b/mockserver/README.md
new file mode 100644
index 00000000..0eab3026
--- /dev/null
+++ b/mockserver/README.md
@@ -0,0 +1,5 @@
+# Mockserver
+
+TCI implementation for [Mockserver](https://github.com/xdev-software/mockserver-neolight).
+
+Note that the classes are abstract and require extension for your corresponding implementation.
diff --git a/mockserver/pom.xml b/mockserver/pom.xml
new file mode 100644
index 00000000..3ae81cc5
--- /dev/null
+++ b/mockserver/pom.xml
@@ -0,0 +1,312 @@
+
+
+ 4.0.0
+
+ software.xdev.tci
+ mockserver
+ 2.0.0-SNAPSHOT
+ jar
+
+ mockserver
+ TCI - mockserver
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 17
+ ${javaVersion}
+
+ UTF-8
+ UTF-8
+
+
+
+
+ software.xdev.tci
+ base
+ 2.0.0-SNAPSHOT
+
+
+
+ software.xdev.mockserver
+ testcontainers
+ 1.0.19
+ compile
+
+
+
+ software.xdev.mockserver
+ client
+ 1.0.19
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 5.0.0
+
+
+ ${project.organization.url}
+
+
+
+ com/mycila/maven/plugin/license/templates/APACHE-2.txt
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+ first
+
+ format
+
+ process-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ ${maven.compiler.release}
+
+ -proc:none
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.2
+
+
+ attach-javadocs
+ package
+
+ jar
+
+
+
+
+ true
+ none
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.3
+
+
+
+
+
+ ignore-service-loading
+
+
+
+ src/main/resources
+
+ META-INF/services/**
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.1
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+ checkstyle
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.26.1
+
+
+
+ ../.config/checkstyle/checkstyle.xml
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+ pmd
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.27.0
+
+ true
+ true
+
+ ../.config/pmd/java/ruleset.xml
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+ 7.15.0
+
+
+ net.sourceforge.pmd
+ pmd-java
+ 7.15.0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 3.6.0
+
+
+
+
+
+
diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/MockServerTCI.java b/mockserver/src/main/java/software/xdev/tci/mockserver/MockServerTCI.java
new file mode 100644
index 00000000..8a3231b0
--- /dev/null
+++ b/mockserver/src/main/java/software/xdev/tci/mockserver/MockServerTCI.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.mockserver;
+
+import software.xdev.mockserver.client.MockServerClient;
+import software.xdev.tci.TCI;
+import software.xdev.testcontainers.mockserver.containers.MockServerContainer;
+
+
+public abstract class MockServerTCI extends TCI
+{
+ protected MockServerClient client;
+
+ protected MockServerTCI(final MockServerContainer container, final String networkAlias)
+ {
+ super(container, networkAlias);
+ }
+
+ @Override
+ public void start(final String containerName)
+ {
+ super.start(containerName);
+ this.client = new MockServerClient(this.getContainer().getHost(), this.getContainer().getServerPort());
+ }
+
+ @Override
+ public void stop()
+ {
+ this.client = null;
+ super.stop();
+ }
+
+ public MockServerClient getClient()
+ {
+ return this.client;
+ }
+}
diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java
new file mode 100644
index 00000000..d3d8b25e
--- /dev/null
+++ b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.mockserver.factory;
+
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+import org.testcontainers.containers.Network;
+
+import software.xdev.tci.factory.ondemand.OnDemandTCIFactory;
+import software.xdev.tci.misc.ContainerMemory;
+import software.xdev.tci.mockserver.MockServerTCI;
+import software.xdev.testcontainers.mockserver.containers.MockServerContainer;
+
+
+public abstract class MockServerTCIFactory
+ extends OnDemandTCIFactory
+{
+ protected MockServerTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final String containerBaseName,
+ final String containerLoggerName)
+ {
+ super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName);
+ }
+
+ @SuppressWarnings("resource")
+ protected MockServerTCIFactory(
+ final BiFunction infraBuilder,
+ final String additionalContainerBaseName,
+ final String additionalLoggerName)
+ {
+ super(
+ infraBuilder,
+ () -> new MockServerContainer()
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)),
+ "mockserver-" + additionalContainerBaseName,
+ "container.mockserver." + additionalLoggerName);
+ }
+
+ public I getNew(
+ final Network network,
+ final String name,
+ final String... networkAliases)
+ {
+ return this.getNew(
+ network, c -> c.withNetworkAliases(networkAliases)
+ .setLogConsumers(List.of(getLogConsumer(this.containerLoggerName + "." + name))));
+ }
+
+ @Override
+ public I getNew(final Network network)
+ {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java
new file mode 100644
index 00000000..14d0346b
--- /dev/null
+++ b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.mockserver.factory;
+
+import java.util.function.BiFunction;
+
+import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
+import software.xdev.tci.misc.ContainerMemory;
+import software.xdev.tci.mockserver.MockServerTCI;
+import software.xdev.testcontainers.mockserver.containers.MockServerContainer;
+
+
+public abstract class PreStartableMockServerTCIFactory
+ extends PreStartableTCIFactory
+{
+ @SuppressWarnings("resource")
+ protected PreStartableMockServerTCIFactory(
+ final BiFunction infraBuilder,
+ final String additionalContainerBaseName,
+ final String additionalLoggerName,
+ final String prestartName)
+ {
+ super(
+ infraBuilder,
+ () -> new MockServerContainer()
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)),
+ "mockserver-" + additionalContainerBaseName,
+ "container.mockserver." + additionalLoggerName,
+ prestartName);
+ }
+}
diff --git a/oidc-server-mock/README.md b/oidc-server-mock/README.md
new file mode 100644
index 00000000..daef09e0
--- /dev/null
+++ b/oidc-server-mock/README.md
@@ -0,0 +1,4 @@
+# OIDC Server Mock
+
+TCI for [OIDC Server Mock](https://github.com/xdev-software/oidc-server-mock).
+
diff --git a/oidc-server-mock/pom.xml b/oidc-server-mock/pom.xml
new file mode 100644
index 00000000..f4110454
--- /dev/null
+++ b/oidc-server-mock/pom.xml
@@ -0,0 +1,305 @@
+
+
+ 4.0.0
+
+ software.xdev.tci
+ oidc-server-mock
+ 2.0.0-SNAPSHOT
+ jar
+
+ oidc-server-mock
+ TCI - oidc-server-mock
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 17
+ ${javaVersion}
+
+ UTF-8
+ UTF-8
+
+
+
+
+ software.xdev.tci
+ base
+ 2.0.0-SNAPSHOT
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ 5.5
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 5.0.0
+
+
+ ${project.organization.url}
+
+
+
+ com/mycila/maven/plugin/license/templates/APACHE-2.txt
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+ first
+
+ format
+
+ process-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ ${maven.compiler.release}
+
+ -proc:none
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.2
+
+
+ attach-javadocs
+ package
+
+ jar
+
+
+
+
+ true
+ none
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.3
+
+
+
+
+
+ ignore-service-loading
+
+
+
+ src/main/resources
+
+ META-INF/services/**
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.1
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+ checkstyle
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.26.1
+
+
+
+ ../.config/checkstyle/checkstyle.xml
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+ pmd
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.27.0
+
+ true
+ true
+
+ ../.config/pmd/java/ruleset.xml
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+ 7.15.0
+
+
+ net.sourceforge.pmd
+ pmd-java
+ 7.15.0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 3.6.0
+
+
+
+
+
+
diff --git a/tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/OIDCTCI.java b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/OIDCTCI.java
similarity index 56%
rename from tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/OIDCTCI.java
rename to oidc-server-mock/src/main/java/software/xdev/tci/oidc/OIDCTCI.java
index f8ca2229..a68cf016 100644
--- a/tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/OIDCTCI.java
+++ b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/OIDCTCI.java
@@ -1,4 +1,19 @@
-package software.xdev.tci.demo.tci.oidc;
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.oidc;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -23,20 +38,27 @@
import org.rnorth.ducttape.unreliables.Unreliables;
import software.xdev.tci.TCI;
-import software.xdev.tci.demo.tci.oidc.containers.OIDCServerContainer;
+import software.xdev.tci.envperf.EnvironmentPerformance;
+import software.xdev.tci.misc.http.HttpClientCloser;
+import software.xdev.tci.oidc.containers.OIDCServerContainer;
public class OIDCTCI extends TCI
{
protected static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(30);
- public static final String DEFAULT_DOMAIN = "example.org";
public static final String CLIENT_ID = OIDCServerContainer.DEFAULT_CLIENT_ID;
public static final String CLIENT_SECRET = OIDCServerContainer.DEFAULT_CLIENT_SECRET;
- public static final String DEFAULT_USER_EMAIL = "test@" + DEFAULT_DOMAIN;
- public static final String DEFAULT_USER_NAME = "Testuser";
- public static final String DEFAULT_USER_PASSWORD = "pwd";
+ protected static final String DEFAULT_DOMAIN = "example.local";
+ protected static final String DEFAULT_USER_EMAIL = "test@" + DEFAULT_DOMAIN;
+ protected static final String DEFAULT_USER_NAME = "Testuser";
+ protected static final String DEFAULT_USER_PASSWORD = "pwd";
+
+ protected boolean shouldAddDefaultUser = true;
+ protected String defaultUserEmail = DEFAULT_USER_EMAIL;
+ protected String defaultUserName = DEFAULT_USER_NAME;
+ protected String defaultUserPassword = DEFAULT_USER_PASSWORD;
public OIDCTCI(final OIDCServerContainer container, final String networkAlias)
{
@@ -47,25 +69,28 @@ public OIDCTCI(final OIDCServerContainer container, final String networkAlias)
public void start(final String containerName)
{
super.start(containerName);
- this.addUser(DEFAULT_USER_EMAIL, DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD);
+ if(this.shouldAddDefaultUser)
+ {
+ this.addUser(this.getDefaultUserEmail(), this.getDefaultUserName(), this.getDefaultUserPassword());
+ }
- // Otherwise app server response may time out as initial requests needs a few seconds
+ // Warm up; Otherwise slow initial response may cause a timeout during tests
this.warmUpWellKnownJWKsEndpoint();
}
public String getDefaultUserEmail()
{
- return DEFAULT_USER_EMAIL;
+ return this.defaultUserEmail;
}
public String getDefaultUserName()
{
- return DEFAULT_USER_NAME;
+ return this.defaultUserName;
}
public String getDefaultUserPassword()
{
- return DEFAULT_USER_PASSWORD;
+ return this.defaultUserPassword;
}
public static String getInternalHttpBaseEndPoint(final String networkAlias)
@@ -83,23 +108,30 @@ public String getExternalHttpBaseEndPoint()
return this.getContainer().getExternalHttpBaseEndPoint();
}
+ @SuppressWarnings("PMD.UseTryWithResources") // Java 17 support
public void warmUpWellKnownJWKsEndpoint()
{
- // NOTE: ON JDK 21+ you should close this!
+ final int slownessFactor = EnvironmentPerformance.cpuSlownessFactor();
final HttpClient httpClient = HttpClient.newBuilder()
- .connectTimeout(Duration.ofSeconds(2L))
+ .connectTimeout(Duration.ofSeconds(1L + slownessFactor))
.build();
- Unreliables.retryUntilSuccess(
- 5,
- () ->
- httpClient.send(
+ try
+ {
+ Unreliables.retryUntilSuccess(
+ 5 + slownessFactor,
+ () -> httpClient.send(
HttpRequest.newBuilder(URI.create(
this.getExternalHttpBaseEndPoint() + "/.well-known/openid-configuration/jwks"))
- .timeout(Duration.ofSeconds(10L))
+ .timeout(Duration.ofSeconds(10L + slownessFactor * 5L))
.GET()
.build(),
HttpResponse.BodyHandlers.discarding()));
+ }
+ finally
+ {
+ HttpClientCloser.close(httpClient);
+ }
}
public void addUser(
@@ -107,18 +139,9 @@ public void addUser(
final String name,
final String pw)
{
- addUser(this.getContainer(), email, name, pw);
- }
-
- protected static void addUser(
- final OIDCServerContainer container,
- final String email,
- final String name,
- final String pw)
- {
- try(final CloseableHttpClient client = createDefaultHttpClient())
+ try(final CloseableHttpClient client = this.createDefaultHttpClient())
{
- final HttpPost post = new HttpPost(container.getExternalHttpBaseEndPoint() + "/api/v1/user");
+ final HttpPost post = new HttpPost(this.getContainer().getExternalHttpBaseEndPoint() + "/api/v1/user");
post.setEntity(new StringEntity("""
{
"SubjectId":"%s",
@@ -161,7 +184,7 @@ protected static void addUser(
}
}
- protected static CloseableHttpClient createDefaultHttpClient()
+ protected CloseableHttpClient createDefaultHttpClient()
{
return HttpClientBuilder.create()
.setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
@@ -172,4 +195,32 @@ protected static CloseableHttpClient createDefaultHttpClient()
.build())
.build();
}
+
+ // region Configure
+
+ protected OIDCTCI withShouldAddDefaultUser(final boolean shouldAddDefaultUser)
+ {
+ this.shouldAddDefaultUser = shouldAddDefaultUser;
+ return this;
+ }
+
+ public OIDCTCI withDefaultUserEmail(final String defaultUserEmail)
+ {
+ this.defaultUserEmail = defaultUserEmail;
+ return this;
+ }
+
+ public OIDCTCI withDefaultUserName(final String defaultUserName)
+ {
+ this.defaultUserName = defaultUserName;
+ return this;
+ }
+
+ public OIDCTCI withDefaultUserPassword(final String defaultUserPassword)
+ {
+ this.defaultUserPassword = defaultUserPassword;
+ return this;
+ }
+
+ // endregion
}
diff --git a/oidc-server-mock/src/main/java/software/xdev/tci/oidc/containers/OIDCServerContainer.java b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/containers/OIDCServerContainer.java
new file mode 100644
index 00000000..4acef4b1
--- /dev/null
+++ b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/containers/OIDCServerContainer.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.oidc.containers;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+
+@SuppressWarnings("java:S2160")
+public class OIDCServerContainer extends GenericContainer
+{
+ public static final int PORT = 8080;
+
+ public static final String DEFAULT_CLIENT_ID = "client-id1";
+ public static final String DEFAULT_CLIENT_SECRET = "client-secret1";
+
+ protected String clientId = DEFAULT_CLIENT_ID;
+ protected String clientSecret = DEFAULT_CLIENT_SECRET;
+ protected List additionalAllowedScopes;
+
+ public OIDCServerContainer()
+ {
+ super(DockerImageName.parse("xdevsoftware/oidc-server-mock:1"));
+ this.addExposedPort(PORT);
+ }
+
+ public OIDCServerContainer withDefaultEnvConfig()
+ {
+ this.addEnv("ASPNETCORE_ENVIRONMENT", "Development");
+ this.addEnv("ASPNET_SERVICES_OPTIONS_INLINE", this.buildAspnetServicesOptionsInline());
+ this.addEnv("SERVER_OPTIONS_INLINE", this.buildServerOptionsInline());
+ this.addEnv("LOGIN_OPTIONS_INLINE", this.buildLoginOptionsInline());
+ this.addEnv("LOGOUT_OPTIONS_INLINE", this.buildLogoutOptionsInline());
+ this.addEnv("CLIENTS_CONFIGURATION_INLINE", this.buildClientsConfigurationInline());
+ return this.self();
+ }
+
+ protected String buildAspnetServicesOptionsInline()
+ {
+ return """
+ {
+ "ForwardedHeadersOptions": {
+ "ForwardedHeaders" : "All"
+ }
+ }
+ """;
+ }
+
+ protected String buildServerOptionsInline()
+ {
+ return """
+ {
+ "AccessTokenJwtType": "JWT",
+ "Discovery": {
+ "ShowKeySet": true
+ },
+ "Authentication": {
+ "CookieSameSiteMode": "Lax",
+ "CheckSessionCookieSameSiteMode": "Lax"
+ }
+ }
+ """;
+ }
+
+ protected String buildLoginOptionsInline()
+ {
+ return """
+ {
+ "AllowRememberLogin": false
+ }
+ """;
+ }
+
+ protected String buildLogoutOptionsInline()
+ {
+ return """
+ {
+ "AutomaticRedirectAfterSignOut": true
+ }
+ """;
+ }
+
+ protected String buildClientsConfigurationInline()
+ {
+ return """
+ [
+ {
+ "ClientId": "%s",
+ "ClientSecrets": [
+ "%s"
+ ],
+ "Description": "TimelineDesc",
+ "AllowedGrantTypes": [
+ "authorization_code",
+ "refresh_token"
+ ],
+ "RedirectUris": [
+ "*"
+ ],
+ "AllowedScopes": [
+ "openid",
+ "profile",
+ "email",
+ "offline_access"
+ %s
+ ],
+ "AlwaysIncludeUserClaimsInIdToken": true,
+ "AllowOfflineAccess": true,
+ "RequirePkce": false
+ }
+ ]
+ """.formatted(
+ this.clientId,
+ this.clientSecret,
+ this.additionalAllowedScopes != null && !this.additionalAllowedScopes.isEmpty()
+ ? ("," + this.additionalAllowedScopes.stream()
+ .map(s -> "\"" + s + "\"")
+ .collect(Collectors.joining(",")))
+ : "");
+ }
+
+ public String getExternalHttpBaseEndPoint()
+ {
+ // noinspection HttpUrlsUsage
+ return "http://"
+ + this.getHost()
+ + ":"
+ + this.getMappedPort(PORT);
+ }
+
+ // region Config
+
+ public OIDCServerContainer withClientId(final String clientId)
+ {
+ this.clientId = clientId;
+ return this;
+ }
+
+ public OIDCServerContainer withClientSecret(final String clientSecret)
+ {
+ this.clientSecret = clientSecret;
+ return this;
+ }
+
+ public OIDCServerContainer withAdditionalAllowedScopes(final List additionalAllowedScopes)
+ {
+ this.additionalAllowedScopes = additionalAllowedScopes;
+ return this;
+ }
+
+ // endregion
+}
diff --git a/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/OIDCTCIFactory.java b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/OIDCTCIFactory.java
new file mode 100644
index 00000000..68461c2d
--- /dev/null
+++ b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/OIDCTCIFactory.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.oidc.factory;
+
+import java.time.Duration;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+import org.apache.hc.core5.http.HttpStatus;
+import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
+import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
+import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
+
+import software.xdev.tci.envperf.EnvironmentPerformance;
+import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
+import software.xdev.tci.factory.prestart.config.PreStartConfig;
+import software.xdev.tci.misc.ContainerMemory;
+import software.xdev.tci.oidc.OIDCTCI;
+import software.xdev.tci.oidc.containers.OIDCServerContainer;
+
+
+public class OIDCTCIFactory extends PreStartableTCIFactory
+{
+ private static final String DEFAULT_CONTAINER_LOGGER_NAME = "container.oidc";
+ private static final String DEFAULT_CONTAINER_BASE_NAME = "oidc";
+ private static final String DEFAULT_NAME = "OIDC";
+
+ public OIDCTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder)
+ {
+ super(infraBuilder, containerBuilder, DEFAULT_CONTAINER_BASE_NAME, DEFAULT_CONTAINER_LOGGER_NAME,
+ DEFAULT_NAME);
+ }
+
+ public OIDCTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final PreStartConfig config,
+ final Timeouts timeouts)
+ {
+ super(
+ infraBuilder, containerBuilder,
+ DEFAULT_CONTAINER_BASE_NAME, DEFAULT_CONTAINER_LOGGER_NAME, DEFAULT_NAME, config, timeouts);
+ }
+
+ public OIDCTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final String containerBaseName,
+ final String containerLoggerName,
+ final String name)
+ {
+ super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName, name);
+ }
+
+ public OIDCTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final String containerBaseName,
+ final String containerLoggerName,
+ final String name,
+ final PreStartConfig config,
+ final Timeouts timeouts)
+ {
+ super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName, name, config, timeouts);
+ }
+
+ public OIDCTCIFactory()
+ {
+ super(
+ OIDCTCI::new,
+ OIDCTCIFactory::createDefaultContainer,
+ DEFAULT_CONTAINER_BASE_NAME,
+ DEFAULT_CONTAINER_LOGGER_NAME,
+ DEFAULT_NAME);
+ }
+
+ @SuppressWarnings({"resource", "checkstyle:MagicNumber"})
+ protected static OIDCServerContainer createDefaultContainer()
+ {
+ return new OIDCServerContainer()
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M))
+ .waitingFor(
+ new WaitAllStrategy()
+ .withStartupTimeout(Duration.ofSeconds(40L + 20L * EnvironmentPerformance.cpuSlownessFactor()))
+ .withStrategy(new HostPortWaitStrategy())
+ .withStrategy(
+ new HttpWaitStrategy()
+ .forPort(OIDCServerContainer.PORT)
+ .forPath("/")
+ .forStatusCode(HttpStatus.SC_OK)
+ .withReadTimeout(Duration.ofSeconds(10))
+ )
+ )
+ .withDefaultEnvConfig();
+ }
+}
diff --git a/pom.xml b/pom.xml
index 6f9afdaf..0bfe8aaa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,9 +4,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
- software.xdev
- tci-base-root
- 1.2.1-SNAPSHOT
+ software.xdev.tci
+ root
+ 2.0.0-SNAPSHOT
pom
@@ -15,9 +15,18 @@
- tci-base
- tci-base-demo
- tci-advanced-demo
+ base
+ bom
+ db-jdbc-orm
+ jul-to-slf4j
+ mockserver
+ oidc-server-mock
+ selenium
+ spring-dao-support
+
+
+ base-demo
+ advanced-demo
@@ -46,7 +55,7 @@
com.puppycrawl.tools
checkstyle
- 10.25.0
+ 10.26.1
@@ -71,7 +80,7 @@
org.apache.maven.plugins
maven-pmd-plugin
- 3.26.0
+ 3.27.0
true
true
@@ -83,12 +92,12 @@
net.sourceforge.pmd
pmd-core
- 7.14.0
+ 7.15.0
net.sourceforge.pmd
pmd-java
- 7.14.0
+ 7.15.0
diff --git a/renovate.json5 b/renovate.json5
index 2d1edde4..2a9899bb 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -4,7 +4,7 @@
"packageRules": [
{
"description": "Ignore project internal dependencies",
- "packagePattern": "^software.xdev:tci-base",
+ "packagePattern": "^software.xdev.tci",
"datasources": [
"maven"
],
@@ -72,6 +72,16 @@
"maven"
],
"groupName": "org.junit"
+ },
+ {
+ "description": "Group org.hibernate.orm",
+ "matchPackagePatterns": [
+ "^org.hibernate.orm"
+ ],
+ "datasources": [
+ "maven"
+ ],
+ "groupName": "org.hibernate.orm"
}
]
}
diff --git a/selenium/README.md b/selenium/README.md
new file mode 100644
index 00000000..3b14ee10
--- /dev/null
+++ b/selenium/README.md
@@ -0,0 +1,11 @@
+# Selenium
+
+TCI for Selenium.
+
+## Features
+
+* All improvements from [xdev-software/testcontainers-selenium](https://github.com/xdev-software/testcontainers-selenium/)
+* Predefined browsers (Firefox, Chromium)
+* NoVNC Support
+* Full scale support for recording videos
+* Browser Logs can be enabled if required
diff --git a/selenium/pom.xml b/selenium/pom.xml
new file mode 100644
index 00000000..b33c016e
--- /dev/null
+++ b/selenium/pom.xml
@@ -0,0 +1,354 @@
+
+
+ 4.0.0
+
+ software.xdev.tci
+ selenium
+ 2.0.0-SNAPSHOT
+ jar
+
+ selenium
+ TCI - selenium
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 17
+ ${javaVersion}
+
+ UTF-8
+ UTF-8
+
+
+
+
+
+ org.seleniumhq.selenium
+ selenium-dependencies-bom
+ 4.34.0
+ pom
+ import
+
+
+
+
+
+
+ software.xdev.tci
+ base
+ 2.0.0-SNAPSHOT
+
+
+ software.xdev.tci
+ jul-to-slf4j
+ 2.0.0-SNAPSHOT
+
+
+
+ software.xdev
+ testcontainers-selenium
+ 1.2.4
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.13.2
+ compile
+
+
+
+
+ org.seleniumhq.selenium
+ selenium-remote-driver
+
+
+
+ io.opentelemetry
+ *
+
+
+
+
+ org.seleniumhq.selenium
+ selenium-support
+
+
+ org.seleniumhq.selenium
+ selenium-firefox-driver
+
+
+ org.seleniumhq.selenium
+ selenium-chrome-driver
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 5.0.0
+
+
+ ${project.organization.url}
+
+
+
+ com/mycila/maven/plugin/license/templates/APACHE-2.txt
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+ first
+
+ format
+
+ process-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ ${maven.compiler.release}
+
+ -proc:none
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.2
+
+
+ attach-javadocs
+ package
+
+ jar
+
+
+
+
+ true
+ none
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.3
+
+
+
+
+
+ ignore-service-loading
+
+
+
+ src/main/resources
+
+ META-INF/services/**
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.1
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+ checkstyle
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.26.1
+
+
+
+ ../.config/checkstyle/checkstyle.xml
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+ pmd
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.27.0
+
+ true
+ true
+
+ ../.config/pmd/java/ruleset.xml
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+ 7.15.0
+
+
+ net.sourceforge.pmd
+ pmd-java
+ 7.15.0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 3.6.0
+
+
+
+
+
+
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java b/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java
new file mode 100644
index 00000000..b19373af
--- /dev/null
+++ b/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.selenium;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.MutableCapabilities;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.bidi.log.LogLevel;
+import org.openqa.selenium.bidi.log.StackTrace;
+import org.openqa.selenium.bidi.module.LogInspector;
+import org.openqa.selenium.remote.Augmenter;
+import org.openqa.selenium.remote.HttpCommandExecutor;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.remote.http.ClientConfig;
+import org.openqa.selenium.remote.http.HttpClient;
+import org.rnorth.ducttape.timeouts.Timeouts;
+import org.rnorth.ducttape.unreliables.Unreliables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.lifecycle.TestDescription;
+
+import software.xdev.tci.TCI;
+import software.xdev.tci.envperf.EnvironmentPerformance;
+import software.xdev.tci.logging.JULtoSLF4JRedirector;
+import software.xdev.tci.selenium.containers.SeleniumBrowserWebDriverContainer;
+
+
+public class BrowserTCI extends TCI
+{
+ private static final Logger LOG = LoggerFactory.getLogger(BrowserTCI.class);
+ public static final Pattern IP_PORT_EXTRACTOR = Pattern.compile("(.*\\/\\/)([0-9a-f\\:\\.]*):(\\d*)(\\/.*)");
+ public static final Set CAPS_TO_PATCH_ADDRESS = Set.of("webSocketUrl", "se:cdp");
+
+ protected final MutableCapabilities capabilities;
+
+ // Use the new world by default
+ // https://www.selenium.dev/documentation/webdriver/bidi
+ protected boolean bidiEnabled = true;
+
+ // Disables the (not standardized) Chrome Dev Tools (CDP) protocol (when bidi is enabled).
+ // CDP requires additional maven dependencies (e.g. selenium-devtools-v137) that are
+ // NOT present and result in a warning.
+ protected boolean deactivateCDPIfPossible = true;
+
+ protected ClientConfig clientConfig = ClientConfig.defaultConfig();
+ protected int webDriverRetryCount = 2;
+ protected int webDriverRetrySec = 30;
+ protected Consumer browserConsoleLogConsumer;
+ protected Set browserConsoleLogLevels;
+
+ protected RemoteWebDriver webDriver;
+ protected LogInspector logInspector;
+
+ public BrowserTCI(
+ final SeleniumBrowserWebDriverContainer container,
+ final String networkAlias,
+ final MutableCapabilities capabilities)
+ {
+ super(container, networkAlias);
+ this.capabilities = capabilities;
+ }
+
+ public BrowserTCI withBidiEnabled(final boolean bidiEnabled)
+ {
+ this.bidiEnabled = bidiEnabled;
+ return this;
+ }
+
+ public BrowserTCI withDeactivateCDPIfPossible(final boolean deactivateCDPIfPossible)
+ {
+ this.deactivateCDPIfPossible = deactivateCDPIfPossible;
+ return this;
+ }
+
+ public BrowserTCI withClientConfig(final ClientConfig clientConfig)
+ {
+ this.clientConfig = Objects.requireNonNull(clientConfig);
+ return this;
+ }
+
+ public BrowserTCI withWebDriverRetryCount(final int webDriverRetryCount)
+ {
+ this.webDriverRetryCount = Math.min(Math.max(webDriverRetryCount, 2), 10);
+ return this;
+ }
+
+ public BrowserTCI withWebDriverRetrySec(final int webDriverRetrySec)
+ {
+ this.webDriverRetrySec = Math.min(Math.max(webDriverRetrySec, 10), 10 * 60);
+ return this;
+ }
+
+ public BrowserTCI withBrowserConsoleLog(
+ final Consumer browserConsoleLogConsumer,
+ final Set browserConsoleLogLevels)
+ {
+ this.browserConsoleLogConsumer = browserConsoleLogConsumer;
+ this.browserConsoleLogLevels = browserConsoleLogLevels;
+ return this;
+ }
+
+ @Override
+ public void start(final String containerName)
+ {
+ super.start(containerName);
+
+ // Selenium uses JUL
+ JULtoSLF4JRedirector.redirect();
+
+ this.initWebDriver();
+ }
+
+ @SuppressWarnings("checkstyle:MagicNumber")
+ protected void initWebDriver()
+ {
+ LOG.debug("Initializing WebDriver");
+ final AtomicInteger retryCounter = new AtomicInteger(1);
+ this.webDriver = Unreliables.retryUntilSuccess(
+ this.webDriverRetryCount,
+ () -> this.tryCreateWebDriver(retryCounter));
+
+ // Default timeout is 5m? -> Single test failure causes up to 10m delays
+ // https://w3c.github.io/webdriver/#timeouts
+ this.webDriver.manage().timeouts().pageLoadTimeout(
+ Duration.ofSeconds(40L + 20L * EnvironmentPerformance.cpuSlownessFactor()));
+
+ // Maximize window
+ this.webDriver.manage().window().maximize();
+
+ this.installBrowserLogInspector();
+ }
+
+ protected RemoteWebDriver tryCreateWebDriver(final AtomicInteger retryCounter)
+ {
+ final ClientConfig config =
+ this.clientConfig.baseUri(this.getContainer().getSeleniumAddressURI());
+ final int tryCount = retryCounter.getAndIncrement();
+ LOG.info(
+ "Creating new WebDriver [retryCount={},retrySec={},clientConfig={}] Try #{}",
+ this.webDriverRetryCount,
+ this.webDriverRetrySec,
+ config,
+ tryCount);
+
+ final HttpClient.Factory factory = HttpCommandExecutor.getDefaultClientFactory();
+ @SuppressWarnings("java:S2095") // Handled by Selenium when quit is called
+ final HttpClient client = factory.createClient(config);
+
+ final HttpCommandExecutor commandExecutor = new HttpCommandExecutor(
+ Map.of(),
+ config,
+ // Constructor without factory does not exist...
+ ignored -> client);
+
+ try
+ {
+ return Timeouts.getWithTimeout(
+ this.webDriverRetrySec,
+ TimeUnit.SECONDS,
+ () -> {
+ this.capabilities.setCapability("webSocketUrl", this.bidiEnabled ? true : null);
+ if(this.bidiEnabled && this.deactivateCDPIfPossible)
+ {
+ this.modifyCapsDisableCDP();
+ }
+
+ final RemoteWebDriver driver =
+ new RemoteWebDriver(commandExecutor, this.capabilities);
+ if(!this.bidiEnabled)
+ {
+ return driver;
+ }
+
+ if(this.deactivateCDPIfPossible)
+ {
+ this.disableCDP(driver);
+ }
+
+ // Create BiDi able driver
+ this.fixCapAddress(driver);
+
+ final Augmenter augmenter = new Augmenter();
+ final WebDriver augmentedWebDriver = augmenter.augment(driver);
+
+ return (RemoteWebDriver)augmentedWebDriver;
+ });
+ }
+ catch(final RuntimeException rex)
+ {
+ // Cancel further communication and abort all connections
+ try
+ {
+ LOG.warn("Encounter problem in try #{} - Terminating communication", tryCount);
+ client.close();
+ factory.cleanupIdleClients();
+ }
+ catch(final Exception ex)
+ {
+ LOG.warn("Failed to cleanup try #{}", tryCount, ex);
+ }
+
+ throw rex;
+ }
+ }
+
+ protected void modifyCapsDisableCDP()
+ {
+ this.capabilities.setCapability("se:cdpEnabled", Boolean.FALSE.toString());
+ }
+
+ protected void disableCDP(final RemoteWebDriver originalDriver)
+ {
+ if(!(originalDriver.getCapabilities() instanceof final MutableCapabilities mutableCapabilities))
+ {
+ return;
+ }
+ mutableCapabilities.setCapability("se:cdp", (Object)null);
+ }
+
+ protected Set getCapsToPatchAddress()
+ {
+ return CAPS_TO_PATCH_ADDRESS;
+ }
+
+ /**
+ * Tries to fix the capabilities, e.g. wrong URLs that must be translated so that communication with the container
+ * works
+ */
+ protected void fixCapAddress(final RemoteWebDriver originalDriver)
+ {
+ if(!(originalDriver.getCapabilities() instanceof final MutableCapabilities mutableCapabilities))
+ {
+ return;
+ }
+
+ for(final String capabilityName : this.getCapsToPatchAddress())
+ {
+ if(mutableCapabilities.getCapability(capabilityName) instanceof final String cdpCap)
+ {
+ final Matcher matcher = IP_PORT_EXTRACTOR.matcher(cdpCap);
+ if(matcher.find())
+ {
+ final String newValue = matcher.group(1)
+ + this.getContainer().getHost()
+ + ":"
+ + this.getContainer().getMappedPort(Integer.parseInt(matcher.group(3)))
+ + matcher.group(4);
+ mutableCapabilities.setCapability(
+ capabilityName,
+ newValue);
+ LOG.debug("Patched cap '{}': '{}' -> '{}'", capabilityName, cdpCap, newValue);
+ }
+ }
+ }
+ }
+
+ protected void installBrowserLogInspector()
+ {
+ if(this.browserConsoleLogConsumer == null || !this.bidiEnabled)
+ {
+ if(this.browserConsoleLogConsumer != null)
+ {
+ LOG.warn("Browser Console Log Consumer is present but BiDi is disabled");
+ }
+ return;
+ }
+
+ LOG.info("Installing Log Inspector");
+
+ this.logInspector = new LogInspector(this.webDriver);
+ final String name = this.getContainer().getContainerNameCleaned();
+ this.logInspector.onConsoleEntry(c -> {
+ if(this.browserConsoleLogLevels == null || this.browserConsoleLogLevels.contains(c.getLevel()))
+ {
+ this.browserConsoleLogConsumer.accept(
+ "[" + name + "] "
+ + c.getLevel().name().toUpperCase() + " "
+ + c.getText()
+ + Optional.ofNullable(c.getStackTrace()) // Note: 2024-11 Stacktrace is not present in FF
+ .map(StackTrace::getCallFrames)
+ .map(cf -> "\n" + cf.stream()
+ .map(s -> "-> "
+ + s.getFunctionName()
+ + "@" + s.getUrl()
+ + " L" + s.getLineNumber()
+ + "C" + s.getColumnNumber()).collect(
+ Collectors.joining("\n")))
+ .orElse(""));
+ }
+ });
+ }
+
+ public Optional getVncAddress()
+ {
+ return Optional.ofNullable(this.getContainer().getVncAddress());
+ }
+
+ public Optional getNoVncAddress()
+ {
+ return Optional.ofNullable(this.getContainer().getNoVncAddress());
+ }
+
+ public RemoteWebDriver getWebDriver()
+ {
+ return this.webDriver;
+ }
+
+ public void afterTest(final TestDescription description, final Optional throwable)
+ {
+ if(this.getContainer() != null)
+ {
+ this.getContainer().afterTest(description, throwable);
+ }
+ }
+
+ @Override
+ public void stop()
+ {
+ if(this.logInspector != null)
+ {
+ final long startMs = System.currentTimeMillis();
+ try
+ {
+ this.logInspector.close();
+ }
+ catch(final Exception e)
+ {
+ LOG.warn("Failed to stop logInspector", e);
+ }
+ finally
+ {
+ LOG.debug("Stopping logInspector driver took {}ms", System.currentTimeMillis() - startMs);
+ }
+ this.logInspector = null;
+ }
+ if(this.webDriver != null)
+ {
+ final long startMs = System.currentTimeMillis();
+ try
+ {
+ this.webDriver.quit();
+ }
+ catch(final Exception e)
+ {
+ LOG.warn("Failed to quit the driver", e);
+ }
+ finally
+ {
+ LOG.debug("Quiting driver took {}ms", System.currentTimeMillis() - startMs);
+ }
+ this.webDriver = null;
+ }
+ super.stop();
+ }
+}
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/TestBrowser.java b/selenium/src/main/java/software/xdev/tci/selenium/TestBrowser.java
new file mode 100644
index 00000000..39e0884f
--- /dev/null
+++ b/selenium/src/main/java/software/xdev/tci/selenium/TestBrowser.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.selenium;
+
+import java.util.function.Supplier;
+
+import org.openqa.selenium.MutableCapabilities;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.firefox.FirefoxOptions;
+import org.openqa.selenium.firefox.FirefoxProfile;
+
+
+public enum TestBrowser
+{
+ FIREFOX(() -> {
+ final FirefoxOptions firefoxOptions = new FirefoxOptions();
+
+ final FirefoxProfile firefoxProfile = new FirefoxProfile();
+ // Allows to type into console without an annoying SELF XSS popup
+ firefoxProfile.setPreference("devtools.selfxss.count", "100");
+ firefoxOptions.setProfile(firefoxProfile);
+
+ return firefoxOptions;
+ }),
+ CHROME(ChromeOptions::new);
+
+ private final Supplier capabilityFactory;
+
+ TestBrowser(final Supplier driverFactory)
+ {
+ this.capabilityFactory = driverFactory;
+ }
+
+ public Supplier getCapabilityFactory()
+ {
+ return this.capabilityFactory;
+ }
+}
diff --git a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/containers/SeleniumBrowserWebDriverContainer.java b/selenium/src/main/java/software/xdev/tci/selenium/containers/SeleniumBrowserWebDriverContainer.java
similarity index 55%
rename from tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/containers/SeleniumBrowserWebDriverContainer.java
rename to selenium/src/main/java/software/xdev/tci/selenium/containers/SeleniumBrowserWebDriverContainer.java
index fec6e8f4..62895dc4 100644
--- a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/containers/SeleniumBrowserWebDriverContainer.java
+++ b/selenium/src/main/java/software/xdev/tci/selenium/containers/SeleniumBrowserWebDriverContainer.java
@@ -1,4 +1,19 @@
-package software.xdev.tci.demo.tci.selenium.containers;
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.selenium.containers;
import java.util.HashSet;
import java.util.Map;
@@ -20,13 +35,31 @@ public class SeleniumBrowserWebDriverContainer
public SeleniumBrowserWebDriverContainer(final Capabilities capabilities)
{
super(
- capabilities,
- Map.of(
+ capabilities, Map.of(
+ // Limit to the core (and most open source) browsers by distinct engines
+ // 1. Firefox uses Gecko
BrowserType.FIREFOX, FIREFOX_IMAGE,
- // Chrome has no ARM64 image (Why Google?) -> Use chromium instead
+ // 2. Everything else is running Chromium/Blink
+ // Chrome has no ARM64 image (embarrassing) -> Use chromium instead
// https://github.com/SeleniumHQ/docker-selenium/discussions/2379
- BrowserType.CHROME, DockerImageName.parse("selenium/standalone-chromium"))
- );
+ BrowserType.CHROME, CHROMIUM_IMAGE
+ // 3. Safari/Webkit is N/A because Apple is doing Apple stuff
+ // https://github.com/SeleniumHQ/docker-selenium/issues/1635
+ // 4. IE/Trident/EdgeHTML is dead
+ // 5. Everything else is irrelevant
+ ));
+ }
+
+ public SeleniumBrowserWebDriverContainer(
+ final Capabilities capabilities,
+ final Map browserDockerImages)
+ {
+ super(capabilities, browserDockerImages);
+ }
+
+ public SeleniumBrowserWebDriverContainer(final DockerImageName dockerImageName)
+ {
+ super(dockerImageName);
}
@Override
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java
new file mode 100644
index 00000000..f35eb6fd
--- /dev/null
+++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.selenium.factory;
+
+import static software.xdev.tci.envperf.EnvironmentPerformance.cpuSlownessFactor;
+
+import java.time.Duration;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.openqa.selenium.MutableCapabilities;
+import org.openqa.selenium.bidi.log.LogLevel;
+import org.openqa.selenium.remote.http.ClientConfig;
+import org.rnorth.ducttape.unreliables.Unreliables;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
+import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
+
+import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
+import software.xdev.tci.factory.prestart.config.PreStartConfig;
+import software.xdev.tci.misc.ContainerMemory;
+import software.xdev.tci.selenium.BrowserTCI;
+import software.xdev.tci.selenium.containers.SeleniumBrowserWebDriverContainer;
+import software.xdev.tci.selenium.factory.config.BrowserTCIFactoryConfig;
+import software.xdev.tci.serviceloading.TCIServiceLoader;
+import software.xdev.testcontainers.selenium.containers.recorder.SeleniumRecordingContainer;
+
+
+public class BrowserTCIFactory extends PreStartableTCIFactory
+{
+ protected final String browserName;
+
+ public BrowserTCIFactory(final MutableCapabilities capabilities)
+ {
+ this(capabilities, TCIServiceLoader.instance().service(BrowserTCIFactoryConfig.class));
+ }
+
+ @SuppressWarnings({"resource", "checkstyle:MagicNumber"})
+ public BrowserTCIFactory(final MutableCapabilities capabilities, final BrowserTCIFactoryConfig config)
+ {
+ super(
+ (c, na) -> new BrowserTCI(c, na, capabilities)
+ .withBidiEnabled(config.bidiEnabled())
+ .withDeactivateCDPIfPossible(config.deactivateCdpIfPossible())
+ .withClientConfig(ClientConfig.defaultConfig()
+ .readTimeout(Duration.ofSeconds(60 + cpuSlownessFactor() * 10L)))
+ .withWebDriverRetryCount(Math.max(Math.min(cpuSlownessFactor(), 5), 1))
+ .withWebDriverRetrySec(25 + cpuSlownessFactor() * 5)
+ .withBrowserConsoleLog(
+ logBrowserConsoleConsumer(config.browserConsoleLogLevel()),
+ config.browserConsoleLogLevel().logLevels()),
+ () -> new SeleniumBrowserWebDriverContainer(capabilities)
+ .withStartRecordingContainerManually(true)
+ .withRecordingDirectory(config.dirForRecords())
+ .withRecordingMode(config.recordingMode())
+ // 2024-04 VNC is no longer required when recording
+ .withDisableVNC(!config.vncEnabled())
+ .withEnableNoVNC(config.vncEnabled())
+ .withRecordingContainerSupplier(t -> new SeleniumRecordingContainer(t)
+ .withFrameRate(10)
+ .withLogConsumer(getLogConsumer("container.browserrecorder." + capabilities.getBrowserName()))
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)))
+ // Without that a mount volume dialog shows up
+ // https://github.com/testcontainers/testcontainers-java/issues/1670
+ .withSharedMemorySize(ContainerMemory.M2G)
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M1G))
+ .withEnv("SE_SCREEN_WIDTH", "1600")
+ .withEnv("SE_SCREEN_HEIGHT", "900")
+ // By default after 5 mins the session is killed and you can't use the container anymore. Cool or?
+ // https://github.com/SeleniumHQ/docker-selenium?tab=readme-ov-file#grid-url-and-session-timeout
+ .withEnv("SE_NODE_SESSION_TIMEOUT", "3600")
+ // Disable local tracing, as we don't need it
+ // https://github.com/SeleniumHQ/docker-selenium/issues/2355
+ .withEnv("SE_ENABLE_TRACING", "false")
+ // Some (AWS) CPUs are completely overloaded with the default 15s timeout -> increase it
+ .waitingFor(new WaitAllStrategy()
+ .withStrategy(new LogMessageWaitStrategy()
+ .withRegEx(".*(Started Selenium Standalone).*\n")
+ .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor())))
+ .withStrategy(new HostPortWaitStrategy())
+ .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor()))),
+ "selenium-" + capabilities.getBrowserName().toLowerCase(),
+ "container.browserwebdriver." + capabilities.getBrowserName(),
+ "Browser-" + capabilities.getBrowserName());
+ this.browserName = capabilities.getBrowserName();
+ }
+
+ public BrowserTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final String containerBaseName,
+ final String containerLoggerName,
+ final String name,
+ final String browserName)
+ {
+ super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName, name);
+ this.browserName = browserName;
+ }
+
+ @SuppressWarnings("java:S107")
+ public BrowserTCIFactory(
+ final BiFunction infraBuilder,
+ final Supplier containerBuilder,
+ final String containerBaseName,
+ final String containerLoggerName,
+ final String name,
+ final PreStartConfig config,
+ final Timeouts timeouts,
+ final String browserName)
+ {
+ super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName, name, config, timeouts);
+ this.browserName = browserName;
+ }
+
+ protected static Consumer logBrowserConsoleConsumer(final BrowserConsoleLogLevel level)
+ {
+ if(!level.active())
+ {
+ return null;
+ }
+ final Logger logger = LoggerFactory.getLogger("container.browser.console");
+ if(!logger.isInfoEnabled())
+ {
+ return null;
+ }
+ return logger::info;
+ }
+
+ @Override
+ protected void postProcessNew(final BrowserTCI infra)
+ {
+ // Start recording container here otherwise there is a lot of blank video
+ final CompletableFuture cfStartRecorder =
+ CompletableFuture.runAsync(() -> infra.getContainer().startRecordingContainer());
+
+ // Docker needs a few milliseconds (usually less than 100) to reconfigure its networks
+ // In the meantime existing connections might fail if we go on immediately
+ // So let's wait a moment here until everything is fine
+ Unreliables.retryUntilSuccess(
+ 10,
+ TimeUnit.SECONDS,
+ () -> infra.getWebDriver().getCurrentUrl());
+
+ cfStartRecorder.join();
+ }
+
+ @Override
+ public String getFactoryName()
+ {
+ return super.getFactoryName() + "-" + this.browserName;
+ }
+
+ public enum BrowserConsoleLogLevel
+ {
+ OFF(Set.of()),
+ ERROR(Set.of(LogLevel.ERROR)),
+ WARN(Set.of(LogLevel.WARNING, LogLevel.ERROR)),
+ INFO(Set.of(LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR)),
+ DEBUG(Set.of(LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARNING, LogLevel.ERROR)),
+ ALL(null);
+
+ // Selenium's LogLevel can't be compared (order is random) -> use Set
+ private final Set logLevels;
+
+ BrowserConsoleLogLevel(final Set logLevels)
+ {
+ this.logLevels = logLevels;
+ }
+
+ public Set logLevels()
+ {
+ return this.logLevels;
+ }
+
+ public boolean active()
+ {
+ return this.logLevels() == null || !this.logLevels().isEmpty();
+ }
+ }
+}
diff --git a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/factory/BrowsersTCIFactory.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowsersTCIFactory.java
similarity index 56%
rename from tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/factory/BrowsersTCIFactory.java
rename to selenium/src/main/java/software/xdev/tci/selenium/factory/BrowsersTCIFactory.java
index 9f155921..c50e98cb 100644
--- a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/factory/BrowsersTCIFactory.java
+++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowsersTCIFactory.java
@@ -1,4 +1,19 @@
-package software.xdev.tci.demo.tci.selenium.factory;
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.selenium.factory;
import java.util.Arrays;
import java.util.Collection;
@@ -10,31 +25,42 @@
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
-import org.openqa.selenium.Capabilities;
+import org.openqa.selenium.MutableCapabilities;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.Network;
import org.testcontainers.images.RemoteDockerImage;
-import software.xdev.tci.demo.tci.selenium.BrowserTCI;
-import software.xdev.tci.demo.tci.selenium.TestBrowser;
-import software.xdev.tci.demo.tci.selenium.containers.SeleniumBrowserWebDriverContainer;
import software.xdev.tci.factory.TCIFactory;
+import software.xdev.tci.logging.JULtoSLF4JRedirector;
+import software.xdev.tci.selenium.BrowserTCI;
+import software.xdev.tci.selenium.TestBrowser;
+import software.xdev.tci.selenium.containers.SeleniumBrowserWebDriverContainer;
import software.xdev.tci.tracing.TCITracer;
import software.xdev.testcontainers.selenium.containers.recorder.SeleniumRecordingContainer;
public class BrowsersTCIFactory implements TCIFactory
{
- private final Map browserFactories = Collections.synchronizedMap(new HashMap<>());
- private boolean alreadyWarmedUp;
+ protected final Map browserFactories = Collections.synchronizedMap(new HashMap<>());
+ protected boolean alreadyWarmedUp;
public BrowsersTCIFactory()
{
- Arrays.stream(TestBrowser.values())
+ this(Arrays.stream(TestBrowser.values())
.map(TestBrowser::getCapabilityFactory)
- .map(Supplier::get)
- .forEach(cap -> this.browserFactories.put(cap.getBrowserName(), new BrowserTCIFactory(cap)));
+ .map(Supplier::get));
+ }
+
+ public BrowsersTCIFactory(final Stream caps)
+ {
+ caps.forEach(cap -> this.browserFactories.put(cap.getBrowserName(), new BrowserTCIFactory(cap)));
+ }
+
+ public BrowsersTCIFactory(final Map browserFactories)
+ {
+ this.browserFactories.putAll(browserFactories);
}
@Override
@@ -55,6 +81,9 @@ protected synchronized void warmUpInternal()
return;
}
+ // Selenium uses JUL
+ JULtoSLF4JRedirector.redirect();
+
this.browserFactories.values().forEach(BrowserTCIFactory::warmUp);
// Pull video recorder
@@ -73,7 +102,10 @@ protected synchronized void warmUpInternal()
}
@SuppressWarnings("resource")
- public BrowserTCI getNew(final Capabilities capabilities, final Network network, final String... networkAliases)
+ public BrowserTCI getNew(
+ final MutableCapabilities capabilities,
+ final Network network,
+ final String... networkAliases)
{
return this.browserFactories.computeIfAbsent(
capabilities.getBrowserName(),
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java
new file mode 100644
index 00000000..f83f64f0
--- /dev/null
+++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.selenium.factory.config;
+
+import java.nio.file.Path;
+
+import software.xdev.tci.selenium.factory.BrowserTCIFactory;
+import software.xdev.testcontainers.selenium.containers.browser.BrowserWebDriverContainer;
+
+
+public interface BrowserTCIFactoryConfig
+{
+ BrowserWebDriverContainer.RecordingMode recordingMode();
+
+ Path dirForRecords();
+
+ boolean vncEnabled();
+
+ boolean bidiEnabled();
+
+ boolean deactivateCdpIfPossible();
+
+ BrowserTCIFactory.BrowserConsoleLogLevel browserConsoleLogLevel();
+}
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java
new file mode 100644
index 00000000..3ffb63d2
--- /dev/null
+++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.selenium.factory.config;
+
+import java.nio.file.Path;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import software.xdev.tci.selenium.factory.BrowserTCIFactory;
+import software.xdev.testcontainers.selenium.containers.browser.BrowserWebDriverContainer;
+
+
+public class DefaultBrowserTCIFactoryConfig implements BrowserTCIFactoryConfig
+{
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultBrowserTCIFactoryConfig.class);
+
+ public static final String RECORD_MODE = "recordMode";
+ public static final String RECORD_DIR = "recordDir";
+ public static final String VNC_ENABLED = "vncEnabled";
+ public static final String BIDI_ENABLED = "bidiEnabled";
+ public static final String DEACTIVATE_CDP_IF_POSSIBLE = "deactivateCdpIfPossible";
+ public static final String BROWSER_CONSOLE_LOG_LEVEL = "browserConsoleLogLevel";
+
+ public static final String DEFAULT_RECORD_DIR = "target/records";
+
+ protected BrowserWebDriverContainer.RecordingMode systemRecordingMode;
+ protected Path dirForRecords;
+ protected Boolean vncEnabled;
+ protected Boolean bidiEnabled;
+ protected Boolean deactivateCdpIfPossible;
+ protected BrowserTCIFactory.BrowserConsoleLogLevel browserConsoleLogLevel;
+
+ protected Optional resolve(final String propertyName)
+ {
+ final String fullPropertyName = "tci.selenium." + propertyName;
+ return Optional.ofNullable(System.getenv(fullPropertyName
+ .replace(".", "_")
+ .toUpperCase(Locale.ENGLISH)))
+ .or(() -> Optional.ofNullable(System.getProperty(fullPropertyName)));
+ }
+
+ protected boolean resolveBool(final String propertyName, final boolean defaultVal)
+ {
+ return this.resolve(propertyName)
+ .map(s -> "1".equals(s) || Boolean.parseBoolean(s))
+ .orElse(defaultVal);
+ }
+
+ @Override
+ public BrowserWebDriverContainer.RecordingMode recordingMode()
+ {
+ if(this.systemRecordingMode == null)
+ {
+ final String resolvedRecordMode = this.resolve(RECORD_MODE).orElse(null);
+ this.systemRecordingMode = Stream.of(BrowserWebDriverContainer.RecordingMode.values())
+ .filter(rm -> rm.toString().equals(resolvedRecordMode))
+ .findFirst()
+ .orElse(BrowserWebDriverContainer.RecordingMode.RECORD_FAILING);
+ LOG.info("Default Recording Mode='{}'", this.systemRecordingMode);
+ }
+ return this.systemRecordingMode;
+ }
+
+ @Override
+ public Path dirForRecords()
+ {
+ if(this.dirForRecords == null)
+ {
+ this.dirForRecords = Path.of(this.resolve(RECORD_DIR).orElse(DEFAULT_RECORD_DIR));
+ final boolean wasCreated = this.dirForRecords.toFile().mkdirs();
+ LOG.info(
+ "Default Directory for records='{}', created={}", this.dirForRecords.toAbsolutePath(),
+ wasCreated);
+ }
+
+ return this.dirForRecords;
+ }
+
+ @Override
+ public boolean vncEnabled()
+ {
+ if(this.vncEnabled == null)
+ {
+ this.vncEnabled = this.resolveBool(VNC_ENABLED, false);
+ LOG.info("VNC enabled={}", this.vncEnabled);
+ }
+ return this.vncEnabled;
+ }
+
+ @Override
+ public boolean bidiEnabled()
+ {
+ if(this.bidiEnabled == null)
+ {
+ this.bidiEnabled = this.resolveBool(BIDI_ENABLED, true);
+ LOG.info("BiDi enabled={}", this.bidiEnabled);
+ }
+ return this.bidiEnabled;
+ }
+
+ @Override
+ public boolean deactivateCdpIfPossible()
+ {
+ if(this.deactivateCdpIfPossible == null)
+ {
+ this.deactivateCdpIfPossible = this.resolveBool(DEACTIVATE_CDP_IF_POSSIBLE, true);
+ LOG.info("DeactivateCDPIfPossible={}", this.deactivateCdpIfPossible);
+ }
+ return this.deactivateCdpIfPossible;
+ }
+
+ @Override
+ public BrowserTCIFactory.BrowserConsoleLogLevel browserConsoleLogLevel()
+ {
+ if(this.browserConsoleLogLevel == null)
+ {
+ this.browserConsoleLogLevel = this.resolve(BROWSER_CONSOLE_LOG_LEVEL)
+ .map(BrowserTCIFactory.BrowserConsoleLogLevel::valueOf)
+ .orElse(BrowserTCIFactory.BrowserConsoleLogLevel.ERROR);
+ LOG.info("BrowserConsoleLogLevel={}", this.browserConsoleLogLevel);
+ }
+ return this.browserConsoleLogLevel;
+ }
+}
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/testbase/SeleniumRecordingExtension.java b/selenium/src/main/java/software/xdev/tci/selenium/testbase/SeleniumRecordingExtension.java
new file mode 100644
index 00000000..21d2b831
--- /dev/null
+++ b/selenium/src/main/java/software/xdev/tci/selenium/testbase/SeleniumRecordingExtension.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.selenium.testbase;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.lifecycle.TestDescription;
+
+import software.xdev.tci.selenium.BrowserTCI;
+
+
+public abstract class SeleniumRecordingExtension implements AfterTestExecutionCallback
+{
+ private static final Logger LOG = LoggerFactory.getLogger(SeleniumRecordingExtension.class);
+
+ protected final Function tciExtractor;
+
+ protected SeleniumRecordingExtension(
+ final Function tciExtractor)
+ {
+ this.tciExtractor = Objects.requireNonNull(tciExtractor);
+ }
+
+ @Override
+ public void afterTestExecution(final ExtensionContext context) throws Exception
+ {
+ final BrowserTCI browserTCI = this.tciExtractor.apply(context);
+ if(browserTCI != null)
+ {
+ LOG.debug("Trying to capture video");
+
+ browserTCI.afterTest(
+ new TestDescription()
+ {
+ @Override
+ public String getTestId()
+ {
+ return this.getFilesystemFriendlyName();
+ }
+
+ @SuppressWarnings("checkstyle:MagicNumber")
+ @Override
+ public String getFilesystemFriendlyName()
+ {
+ final String testClassName =
+ this.cleanForFilename(context.getRequiredTestClass().getSimpleName());
+ final String displayName = this.cleanForFilename(context.getDisplayName());
+ return System.currentTimeMillis()
+ + "_"
+ + testClassName
+ + "_"
+ // Cut off otherwise file name is too long
+ + displayName.substring(0, Math.min(displayName.length(), 200));
+ }
+
+ private String cleanForFilename(final String str)
+ {
+ return str.replace(' ', '_')
+ .replaceAll("[^A-Za-z0-9#_-]", "")
+ .toLowerCase();
+ }
+ }, context.getExecutionException());
+ }
+ LOG.debug("AfterTestExecution done");
+ }
+}
diff --git a/selenium/src/main/resources/META-INF/services/software.xdev.tci.selenium.factory.config.BrowserTCIFactoryConfig b/selenium/src/main/resources/META-INF/services/software.xdev.tci.selenium.factory.config.BrowserTCIFactoryConfig
new file mode 100644
index 00000000..39a85b4e
--- /dev/null
+++ b/selenium/src/main/resources/META-INF/services/software.xdev.tci.selenium.factory.config.BrowserTCIFactoryConfig
@@ -0,0 +1 @@
+software.xdev.tci.selenium.factory.config.DefaultBrowserTCIFactoryConfig
diff --git a/spring-dao-support/README.md b/spring-dao-support/README.md
new file mode 100644
index 00000000..bd110785
--- /dev/null
+++ b/spring-dao-support/README.md
@@ -0,0 +1,3 @@
+# Spring DAO Support
+
+Provides support for injecting DAO using Spring.
diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml
new file mode 100644
index 00000000..1b6c70c2
--- /dev/null
+++ b/spring-dao-support/pom.xml
@@ -0,0 +1,319 @@
+
+
+ 4.0.0
+
+ software.xdev.tci
+ spring-dao-support
+ 2.0.0-SNAPSHOT
+ jar
+
+ spring-dao-support
+ TCI - spring-dao-support
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 17
+ ${javaVersion}
+
+ UTF-8
+ UTF-8
+
+
+
+
+ software.xdev.tci
+ db-jdbc-orm
+ 2.0.0-SNAPSHOT
+
+
+
+ org.javassist
+ javassist
+ 3.30.2-GA
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.13.2
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.17
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 5.0.0
+
+
+ ${project.organization.url}
+
+
+
+ com/mycila/maven/plugin/license/templates/APACHE-2.txt
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+ first
+
+ format
+
+ process-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.14.0
+
+ ${maven.compiler.release}
+
+ -proc:none
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.2
+
+
+ attach-javadocs
+ package
+
+ jar
+
+
+
+
+ true
+ none
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.3
+
+
+
+
+
+ ignore-service-loading
+
+
+
+ src/main/resources
+
+ META-INF/services/**
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.1
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.8.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+ checkstyle
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 10.26.1
+
+
+
+ ../.config/checkstyle/checkstyle.xml
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+ pmd
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.27.0
+
+ true
+ true
+
+ ../.config/pmd/java/ruleset.xml
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+ 7.15.0
+
+
+ net.sourceforge.pmd
+ pmd-java
+ 7.15.0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 3.6.0
+
+
+
+
+
+
diff --git a/tci-advanced-demo/persistence-it/src/test/java/software/xdev/tci/demo/persistence/base/DAOInjector.java b/spring-dao-support/src/main/java/software/xdev/tci/dao/DAOInjector.java
similarity index 61%
rename from tci-advanced-demo/persistence-it/src/test/java/software/xdev/tci/demo/persistence/base/DAOInjector.java
rename to spring-dao-support/src/main/java/software/xdev/tci/dao/DAOInjector.java
index d725a290..05e527ea 100644
--- a/tci-advanced-demo/persistence-it/src/test/java/software/xdev/tci/demo/persistence/base/DAOInjector.java
+++ b/spring-dao-support/src/main/java/software/xdev/tci/dao/DAOInjector.java
@@ -1,4 +1,19 @@
-package software.xdev.tci.demo.persistence.base;
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package software.xdev.tci.dao;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@@ -6,6 +21,7 @@
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
@@ -18,9 +34,7 @@
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
-import software.xdev.tci.demo.persistence.jpa.dao.BaseDAO;
-import software.xdev.tci.demo.persistence.jpa.dao.TransactionReflector;
-import software.xdev.tci.demo.tci.db.persistence.TransactionExecutor;
+import software.xdev.tci.db.persistence.TransactionExecutor;
/**
@@ -33,6 +47,20 @@ public class DAOInjector
{
private static final Logger LOG = LoggerFactory.getLogger(DAOInjector.class);
+ protected final Class> baseDAOClazz;
+ protected final ThrowingFieldSupplier fEntityManagerInBaseDAOSupplier;
+ protected final HandleSpecialFields handleSpecialFields;
+
+ public DAOInjector(
+ final Class> baseDAOClazz,
+ final ThrowingFieldSupplier fEntityManagerInBaseDAOSupplier,
+ final HandleSpecialFields handleSpecialFields)
+ {
+ this.baseDAOClazz = Objects.requireNonNull(baseDAOClazz);
+ this.fEntityManagerInBaseDAOSupplier = Objects.requireNonNull(fEntityManagerInBaseDAOSupplier);
+ this.handleSpecialFields = handleSpecialFields;
+ }
+
public void doInjections(
final Class> clazz,
final Supplier emSupplier,
@@ -40,7 +68,7 @@ public void doInjections(
{
this.collectAllDeclaredFields(clazz).stream()
.filter(field -> field.isAnnotationPresent(Autowired.class))
- .filter(field -> BaseDAO.class.isAssignableFrom(field.getType()))
+ .filter(field -> this.baseDAOClazz.isAssignableFrom(field.getType()))
.forEach(field -> {
final Class> fieldType = field.getType();
final EntityManager em = emSupplier.get();
@@ -52,30 +80,17 @@ public void doInjections(
? this.createProxiedDAO(fieldType, em, originalDAO)
: originalDAO;
- this.collectAllDeclaredFields(fieldType)
- .stream()
- .filter(f -> TransactionReflector.class.equals(f.getType()))
- .forEach(f -> this.setIntoField(dao, f, new TransactionReflector()
- {
- @Override
- public void runWithTransaction(final Runnable runnable)
- {
- new TransactionExecutor(em).execWithTransaction(runnable);
- }
-
- @Override
- public T runWithTransaction(final Supplier supplier)
- {
- return new TransactionExecutor(em).execWithTransaction(supplier);
- }
- }));
+ if(this.handleSpecialFields != null)
+ {
+ this.handleSpecialFields.handle(this, this.collectAllDeclaredFields(fieldType), dao, em);
+ }
this.setIntoField(instanceSupplier.get(), field, dao);
});
}
@SuppressWarnings("PMD.PreserveStackTrace")
- private Object createProxiedDAO(final Class> fieldType, final EntityManager em, final Object original)
+ protected Object createProxiedDAO(final Class> fieldType, final EntityManager em, final Object original)
{
// java.lang.reflect.Proxy only proxies interfaces and doesn't work here!
// https://stackoverflow.com/a/3292208
@@ -92,7 +107,7 @@ private Object createProxiedDAO(final Class> fieldType, final EntityManager em
}
catch(final IllegalAccessException | InvocationTargetException e)
{
- throw new RuntimeException(e);
+ throw new IllegalStateException("Failed to invoke", e);
}
};
if(em.getTransaction().isActive())
@@ -114,19 +129,19 @@ private Object createProxiedDAO(final Class> fieldType, final EntityManager em
{
return this.setIntoField(
factory.create(new Class>[0], new Object[0], methodHandler),
- BaseDAO.class.getDeclaredField("em"),
+ this.fEntityManagerInBaseDAOSupplier.field(),
em);
}
- catch(final NoSuchMethodException | InstantiationException | IllegalAccessException
- | InvocationTargetException | NoSuchFieldException e2)
+ catch(final NoSuchFieldException | NoSuchMethodException | InstantiationException | IllegalAccessException
+ | InvocationTargetException e2)
{
- throw new RuntimeException("Failed to proxy dao", e2);
+ throw new IllegalStateException("Failed to proxy dao", e2);
}
}
}
@SuppressWarnings("PMD.PreserveStackTrace")
- private Object createDAO(final Class> fieldType, final EntityManager em)
+ protected Object createDAO(final Class> fieldType, final EntityManager em)
{
try
{
@@ -142,7 +157,7 @@ private Object createDAO(final Class> fieldType, final EntityManager em)
{
return this.setIntoField(
fieldType.getConstructor().newInstance(),
- BaseDAO.class.getDeclaredField("em"),
+ this.fEntityManagerInBaseDAOSupplier.field(),
em);
}
catch(final NoSuchFieldException | NoSuchMethodException | InstantiationException
@@ -153,7 +168,7 @@ private Object createDAO(final Class> fieldType, final EntityManager em)
}
}
- private Set collectAllDeclaredFields(final Class> clazz)
+ protected Set collectAllDeclaredFields(final Class> clazz)
{
final Set fields = new HashSet<>();
Class> currentClazz = clazz;
@@ -165,7 +180,7 @@ private Set collectAllDeclaredFields(final Class> clazz)
return fields;
}
- private Set collectAllDeclaredMethods(final Class> clazz)
+ protected Set collectAllDeclaredMethods(final Class> clazz)
{
final Set methods = new HashSet<>();
Class> currentClazz = clazz;
@@ -177,7 +192,8 @@ private Set collectAllDeclaredMethods(final Class> clazz)
return methods;
}
- private Object setIntoField(final Object instance, final Field field, final Object newValue)
+ @SuppressWarnings("java:S3011")
+ public Object setIntoField(final Object instance, final Field field, final Object newValue)
{
field.setAccessible(true);
try
@@ -186,8 +202,21 @@ private Object setIntoField(final Object instance, final Field field, final Obje
}
catch(final IllegalAccessException iae)
{
- throw new RuntimeException(iae);
+ throw new IllegalStateException("Failed to set into field", iae);
}
return instance;
}
+
+ @FunctionalInterface
+ public interface ThrowingFieldSupplier
+ {
+ Field field() throws NoSuchFieldException;
+ }
+
+
+ @FunctionalInterface
+ public interface HandleSpecialFields
+ {
+ void handle(DAOInjector self, Set fields, Object dao, EntityManager em);
+ }
}
diff --git a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/DBTCI.java b/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/DBTCI.java
deleted file mode 100644
index 65e82977..00000000
--- a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/DBTCI.java
+++ /dev/null
@@ -1,267 +0,0 @@
-package software.xdev.tci.demo.tci.db;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
-
-import org.hibernate.cfg.PersistenceSettings;
-import org.hibernate.hikaricp.internal.HikariCPConnectionProvider;
-import org.mariadb.jdbc.Driver;
-import org.mariadb.jdbc.MariaDbDataSource;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testcontainers.containers.GenericContainer;
-
-import software.xdev.tci.TCI;
-import software.xdev.tci.demo.persistence.FlywayInfo;
-import software.xdev.tci.demo.persistence.FlywayMigration;
-import software.xdev.tci.demo.tci.db.containers.DBContainer;
-import software.xdev.tci.demo.tci.db.persistence.EntityManagerController;
-import software.xdev.tci.demo.tci.db.persistence.hibernate.CachingStandardScanner;
-
-
-public class DBTCI extends TCI
-{
- private static final Logger LOG = LoggerFactory.getLogger(DBTCI.class);
-
- public static final String DB_DATABASE = "test";
- public static final String DB_USERNAME = "test";
- @SuppressWarnings("java:S2068") // This is a test calm down
- public static final String DB_PASSWORD = "test";
-
- private final boolean migrateAndInitializeEMC;
-
- protected EntityManagerController emc;
-
- public DBTCI(
- final DBContainer container,
- final String networkAlias,
- final boolean migrateAndInitializeEMC)
- {
- super(container, networkAlias);
- this.migrateAndInitializeEMC = migrateAndInitializeEMC;
- }
-
- public boolean isMigrateAndInitializeEMC()
- {
- return this.migrateAndInitializeEMC;
- }
-
- @Override
- public void start(final String containerName)
- {
- super.start(containerName);
- if(this.migrateAndInitializeEMC)
- {
- // Do basic migrations async
- LOG.debug("Running migration to basic structure");
- this.migrateDatabase(FlywayInfo.FLYWAY_LOOKUP_STRUCTURE);
- LOG.info("Migration executed");
-
- // Create EMC in background to improve performance (~5s)
- LOG.debug("Initializing EntityManagerController...");
- this.getEMC();
- LOG.info("Initialized EntityManagerController");
- }
- }
-
- @Override
- public void stop()
- {
- if(this.emc != null)
- {
- try
- {
- this.emc.close();
- }
- catch(final Exception ex)
- {
- LOG.warn("Failed to close EntityManagerController", ex);
- }
- this.emc = null;
- }
- super.stop();
- }
-
- public EntityManagerController getEMC()
- {
- if(this.emc == null)
- {
- this.initEMCIfRequired();
- }
-
- return this.emc;
- }
-
- protected synchronized void initEMCIfRequired()
- {
- if(this.emc == null)
- {
- this.emc = EntityManagerController.createForStandalone(
- Driver.class.getName(),
- // Use production-ready pool otherwise Hibernate warnings occur
- HikariCPConnectionProvider.class.getName(),
- this.getExternalJDBCUrl(),
- DB_USERNAME,
- DB_PASSWORD,
- Map.ofEntries(
- // Use caching scanner to massively improve performance (this way the scanning only happens once)
- Map.entry(PersistenceSettings.SCANNER, CachingStandardScanner.instance())
- )
- );
- }
- }
-
- public static String getInternalJDBCUrl(final String networkAlias)
- {
- return "jdbc:mariadb://" + networkAlias + ":" + DBContainer.PORT + "/" + DB_DATABASE;
- }
-
- public String getExternalJDBCUrl()
- {
- return this.getContainer().getJdbcUrl();
- }
-
- /**
- * Creates a new {@link EntityManager} with an internal {@link EntityManagerFactory}, which can be used to load and
- * save data in the database for the test.
- *
- *
- * It may be a good idea to close the EntityManager, when you're finished with it.
- *
- *
- * All created EntityManager are automatically cleaned up when the test is finished.
- *
- *
- * @return EntityManager
- */
- public EntityManager createEntityManager()
- {
- return this.getEMC().createEntityManager();
- }
-
- public void useNewEntityManager(final Consumer action)
- {
- try(final EntityManager em = this.createEntityManager())
- {
- action.accept(em);
- }
- }
-
- @SuppressWarnings("java:S6437") // This is a test calm down
- public MariaDbDataSource createDataSource()
- {
- final MariaDbDataSource dataSource = new MariaDbDataSource();
- try
- {
- dataSource.setUser(DB_USERNAME);
- dataSource.setPassword(DB_PASSWORD);
- dataSource.setUrl(this.getExternalJDBCUrl());
- }
- catch(final SQLException e)
- {
- throw new IllegalStateException("Invalid container setup", e);
- }
- return dataSource;
- }
-
- public void migrateDatabase(final Collection locations)
- {
- this.migrateDatabase(locations.toArray(String[]::new));
- }
-
- public void migrateDatabase(final String... locations)
- {
- new FlywayMigration().migrate(conf ->
- {
- conf.dataSource(this.createDataSource());
- conf.locations(locations);
- });
- }
-
- @SuppressWarnings("PMD.InvalidLogMessageFormat") // % is not used for formatting
- public void logDataBaseInfo()
- {
- if(Optional.ofNullable(this.getContainer()).map(GenericContainer::getContainerId).isPresent())
- {
- return;
- }
-
- final Logger logger = LoggerFactory.getLogger(DBTCI.class.getName() + ".sqlstatus");
- if(!logger.isInfoEnabled())
- {
- return;
- }
-
- logger.info("===== SERVER INFO =====");
- try(final Connection conn = this.createDataSource().getConnection();
- final Statement stmt = conn.createStatement())
- {
- logger.info("> SHOW PROCESSLIST <");
- this.printDatabaseInfo(stmt.executeQuery("SHOW PROCESSLIST"), logger);
-
- if(logger.isTraceEnabled())
- {
- logger.info("> SHOW STATUS (TRACE) <");
- this.printDatabaseInfo(stmt.executeQuery("SHOW STATUS"), logger);
- }
- else if(logger.isDebugEnabled())
- {
- logger.info("> SHOW STATUS LIKE '%onn%' (DEBUG) <");
- this.printDatabaseInfo(stmt.executeQuery("SHOW STATUS LIKE '%onn%'"), logger);
- }
- }
- catch(final Exception ex)
- {
- logger.warn("Unable to print database info", ex);
- }
- finally
- {
- logger.info("=======================");
- }
- }
-
- @SuppressWarnings("java:S2629") // is already checked when invoked
- private void printDatabaseInfo(final ResultSet rs, final Logger logger) throws SQLException
- {
- final int colCount = rs.getMetaData().getColumnCount();
-
- logger.info(
- IntStream.range(1, colCount + 1).mapToObj(id ->
- {
- try
- {
- return rs.getMetaData().getColumnName(id);
- }
- catch(final SQLException e)
- {
- return "--Exception--";
- }
- }).collect(Collectors.joining(",")));
- while(rs.next())
- {
- logger.info(
- IntStream.range(1, colCount + 1).mapToObj(id ->
- {
- try
- {
- return rs.getString(id);
- }
- catch(final SQLException e)
- {
- return "--Exception--";
- }
- }).collect(Collectors.joining(",")));
- }
- }
-}
diff --git a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/datageneration/DataGenerator.java b/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/datageneration/DataGenerator.java
deleted file mode 100644
index 3a5bb8aa..00000000
--- a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/datageneration/DataGenerator.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package software.xdev.tci.demo.tci.db.datageneration;
-
-public interface DataGenerator
-{
- // just marker
-}
diff --git a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java b/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java
deleted file mode 100644
index b58d2684..00000000
--- a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/factory/DBTCIFactory.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package software.xdev.tci.demo.tci.db.factory;
-
-import java.sql.Connection;
-import java.sql.Statement;
-import java.util.concurrent.TimeUnit;
-
-import org.rnorth.ducttape.unreliables.Unreliables;
-
-import software.xdev.tci.demo.tci.db.DBTCI;
-import software.xdev.tci.demo.tci.db.containers.DBContainer;
-import software.xdev.tci.demo.tci.db.containers.DBContainerBuilder;
-import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
-import software.xdev.tci.factory.prestart.snapshoting.CommitedImageSnapshotManager;
-import software.xdev.tci.misc.ContainerMemory;
-
-
-public class DBTCIFactory extends PreStartableTCIFactory
-{
- public DBTCIFactory()
- {
- this(true);
- }
-
- @SuppressWarnings("resource")
- public DBTCIFactory(final boolean migrateAndInitializeEMC)
- {
- super(
- (c, n) -> new DBTCI(c, n, migrateAndInitializeEMC),
- () -> new DBContainer(DBContainerBuilder.getBuiltImageName())
- .withDatabaseName(DBTCI.DB_DATABASE)
- .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)),
- "db-mariadb",
- "container.db",
- "DB");
- this.withSnapshotManager(new CommitedImageSnapshotManager("/var/lib/mysql"));
- }
-
- @Override
- protected void postProcessNew(final DBTCI infra)
- {
- // Docker needs a few milliseconds (usually less than 100) to reconfigure its networks
- // In the meantime existing connections might fail if we go on immediately
- // So let's wait a moment here until everything is fine
- Unreliables.retryUntilSuccess(
- 10,
- TimeUnit.SECONDS,
- () -> {
- final String testQuery = infra.getContainer().getTestQueryString();
- try(final Connection con = infra.createDataSource().getConnection();
- final Statement statement = con.createStatement())
- {
- statement.executeQuery(testQuery).getMetaData();
- }
-
- if(infra.isMigrateAndInitializeEMC())
- {
- // Check EMC if pool connections work
- infra.useNewEntityManager(em -> em.createNativeQuery(testQuery).getResultList());
- }
- return null;
- });
- }
-}
diff --git a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/EntityManagerController.java b/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/EntityManagerController.java
deleted file mode 100644
index 626b3c03..00000000
--- a/tci-advanced-demo/tci-db/src/main/java/software/xdev/tci/demo/tci/db/persistence/EntityManagerController.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package software.xdev.tci.demo.tci.db.persistence;
-
-import static java.util.Map.entry;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import jakarta.persistence.Entity;
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
-import jakarta.persistence.spi.ClassTransformer;
-import jakarta.persistence.spi.PersistenceUnitTransactionType;
-
-import org.hibernate.cfg.JdbcSettings;
-import org.hibernate.jpa.HibernatePersistenceProvider;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo;
-
-import software.xdev.tci.demo.persistence.config.DefaultJPAConfig;
-import software.xdev.tci.demo.persistence.util.DisableHibernateFormatMapper;
-
-
-/**
- * Handles the creation and destruction of {@link EntityManager}s.
- *
- * This should only be used when a {@link EntityManager} has to be created manually, e.g. when not running on an
- * AppServer.
- */
-public class EntityManagerController implements AutoCloseable
-{
- private static final Logger LOG = LoggerFactory.getLogger(EntityManagerController.class);
-
- private static Set cachedEntityClassNames;
-
- protected final List activeEms = Collections.synchronizedList(new ArrayList<>());
- protected final EntityManagerFactory emf;
-
- public EntityManagerController(final EntityManagerFactory emf)
- {
- this.emf = Objects.requireNonNull(emf);
- }
-
- /**
- * Creates a new {@link EntityManager} with an internal {@link EntityManagerFactory}, which can be used to load and
- * save data in the database.
- *
- *
- * It may be a good idea to close the EntityManager, when you're finished with it.
- *
- *
- * All created EntityManager are automatically cleaned up once {@link #close()} is called.
- *
- *
- * @return EntityManager
- */
- public EntityManager createEntityManager()
- {
- final EntityManager em = this.emf.createEntityManager();
- this.activeEms.add(em);
-
- return em;
- }
-
- @Override
- public void close()
- {
- LOG.debug("Shutting down resources");
- this.activeEms.forEach(em ->
- {
- try
- {
- if(em.getTransaction() != null && em.getTransaction().isActive())
- {
- em.getTransaction().rollback();
- }
- em.close();
- }
- catch(final Exception e)
- {
- LOG.warn("Unable to close EntityManager", e);
- }
- });
-
- LOG.debug("Cleared {}x EntityManagers", this.activeEms.size());
-
- this.activeEms.clear();
-
- try
- {
- this.emf.close();
- LOG.debug("Released EntityManagerFactory");
- }
- catch(final Exception e)
- {
- LOG.error("Failed to release EntityManagerFactory", e);
- }
- }
-
- public static EntityManagerController createForStandalone(
- final String driverFullClassName,
- final String connectionProviderClassName,
- final String jdbcUrl,
- final String username,
- final String password,
- final Map additionalConfig
- )
- {
- return createForStandalone(
- driverFullClassName,
- connectionProviderClassName,
- "Test",
- jdbcUrl,
- username,
- password,
- additionalConfig);
- }
-
- public static EntityManagerController createForStandalone(
- final String driverFullClassName,
- final String connectionProviderClassName,
- final String persistenceUnitName,
- final String jdbcUrl,
- final String username,
- final String password,
- final Map additionalConfig
- )
- {
- final MutablePersistenceUnitInfo persistenceUnitInfo = new MutablePersistenceUnitInfo()
- {
- @Override
- public void addTransformer(final ClassTransformer classTransformer)
- {
- // Do nothing
- }
-
- @Override
- public ClassLoader getNewTempClassLoader()
- {
- return null;
- }
- };
- persistenceUnitInfo.setTransactionType(PersistenceUnitTransactionType.RESOURCE_LOCAL);
- persistenceUnitInfo.setPersistenceUnitName(persistenceUnitName);
- persistenceUnitInfo.setPersistenceProviderClassName(HibernatePersistenceProvider.class.getName());
- if(cachedEntityClassNames == null)
- {
- cachedEntityClassNames = AnnotatedClassFinder.find(DefaultJPAConfig.ENTITY_PACKAGE, Entity.class)
- .stream()
- .map(Class::getName)
- .collect(Collectors.toSet());
- }
- try
- {
- Collections.list(EntityManagerController.class
- .getClassLoader()
- .getResources(""))
- .forEach(persistenceUnitInfo::addJarFileUrl);
- }
- catch(final IOException ioe)
- {
- throw new UncheckedIOException(ioe);
- }
-
- final Map properties = new HashMap<>(Map.ofEntries(
- entry(JdbcSettings.JAKARTA_JDBC_DRIVER, driverFullClassName),
- entry(JdbcSettings.JAKARTA_JDBC_URL, jdbcUrl),
- entry(JdbcSettings.JAKARTA_JDBC_USER, username),
- entry(JdbcSettings.JAKARTA_JDBC_PASSWORD, password)
- ));
- Optional.ofNullable(connectionProviderClassName)
- .ifPresent(p -> properties.put(JdbcSettings.CONNECTION_PROVIDER, connectionProviderClassName));
- properties.putAll(DisableHibernateFormatMapper.properties());
- return new EntityManagerController(
- new HibernatePersistenceProvider()
- .createContainerEntityManagerFactory(
- persistenceUnitInfo,
- properties));
- }
-}
diff --git a/tci-advanced-demo/tci-oidc/README.md b/tci-advanced-demo/tci-oidc/README.md
deleted file mode 100644
index b2cd739c..00000000
--- a/tci-advanced-demo/tci-oidc/README.md
+++ /dev/null
@@ -1 +0,0 @@
-This module contains the TCI for the OIDC server
diff --git a/tci-advanced-demo/tci-oidc/pom.xml b/tci-advanced-demo/tci-oidc/pom.xml
deleted file mode 100644
index 113df563..00000000
--- a/tci-advanced-demo/tci-oidc/pom.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
- 4.0.0
-
- software.xdev.tci.demo
- tci-advanced-demo
- 1.2.1-SNAPSHOT
-
- tci-oidc
-
-
-
- software.xdev.tci.demo
- tci-testcontainers
-
-
-
- org.apache.httpcomponents.client5
- httpclient5
-
-
-
diff --git a/tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/containers/OIDCServerContainer.java b/tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/containers/OIDCServerContainer.java
deleted file mode 100644
index 69c05708..00000000
--- a/tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/containers/OIDCServerContainer.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package software.xdev.tci.demo.tci.oidc.containers;
-
-import org.testcontainers.containers.GenericContainer;
-import org.testcontainers.utility.DockerImageName;
-
-
-public class OIDCServerContainer extends GenericContainer
-{
- public static final int PORT = 8080;
-
- public static final String DEFAULT_CLIENT_ID = "client-id1";
- public static final String DEFAULT_CLIENT_SECRET = "client-secret1";
-
- public OIDCServerContainer()
- {
- super(DockerImageName.parse("xdevsoftware/oidc-server-mock:1"));
- this.addExposedPort(PORT);
- }
-
- public OIDCServerContainer withDefaultEnvConfig()
- {
- this.addEnv("ASPNETCORE_ENVIRONMENT", "Development");
- this.addEnv("ASPNET_SERVICES_OPTIONS_INLINE", """
- {
- "ForwardedHeadersOptions": {
- "ForwardedHeaders" : "All"
- }
- }
- """);
- this.addEnv("SERVER_OPTIONS_INLINE", """
- {
- "AccessTokenJwtType": "JWT",
- "Discovery": {
- "ShowKeySet": true
- },
- "Authentication": {
- "CookieSameSiteMode": "Lax",
- "CheckSessionCookieSameSiteMode": "Lax"
- }
- }
- """);
- this.addEnv("LOGIN_OPTIONS_INLINE", """
- {
- "AllowRememberLogin": false
- }
- """);
- this.addEnv("LOGOUT_OPTIONS_INLINE", """
- {
- "AutomaticRedirectAfterSignOut": true
- }
- """);
- this.addEnv("CLIENTS_CONFIGURATION_INLINE", """
- [
- {
- "ClientId": "%s",
- "ClientSecrets": [
- "%s"
- ],
- "Description": "Desc",
- "AllowedGrantTypes": [
- "authorization_code",
- "refresh_token"
- ],
- "RedirectUris": [
- "*"
- ],
- "AllowedScopes": [
- "openid",
- "profile",
- "email",
- "offline_access"
- ],
- "AlwaysIncludeUserClaimsInIdToken": true,
- "AllowOfflineAccess": true,
- "RequirePkce": false
- }
- ]
- """.formatted(DEFAULT_CLIENT_ID, DEFAULT_CLIENT_SECRET));
- return this.self();
- }
-
- public String getExternalHttpBaseEndPoint()
- {
- // noinspection HttpUrlsUsage
- return "http://"
- + this.getHost()
- + ":"
- + this.getMappedPort(PORT);
- }
-}
diff --git a/tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/factory/OIDCTCIFactory.java b/tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/factory/OIDCTCIFactory.java
deleted file mode 100644
index 3f180beb..00000000
--- a/tci-advanced-demo/tci-oidc/src/main/java/software/xdev/tci/demo/tci/oidc/factory/OIDCTCIFactory.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package software.xdev.tci.demo.tci.oidc.factory;
-
-import java.time.Duration;
-
-import org.apache.hc.core5.http.HttpStatus;
-import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
-import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
-import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
-
-import software.xdev.tci.demo.tci.oidc.OIDCTCI;
-import software.xdev.tci.demo.tci.oidc.containers.OIDCServerContainer;
-import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
-import software.xdev.tci.misc.ContainerMemory;
-
-
-public class OIDCTCIFactory extends PreStartableTCIFactory
-{
- @SuppressWarnings("resource")
- public OIDCTCIFactory()
- {
- super(
- OIDCTCI::new,
- () -> new OIDCServerContainer()
- .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M))
- .waitingFor(
- new WaitAllStrategy()
- .withStartupTimeout(Duration.ofMinutes(1))
- .withStrategy(new HostPortWaitStrategy())
- .withStrategy(
- new HttpWaitStrategy()
- .forPort(OIDCServerContainer.PORT)
- .forPath("/")
- .forStatusCode(HttpStatus.SC_OK)
- .withReadTimeout(Duration.ofSeconds(10))
- )
- )
- .withDefaultEnvConfig(),
- "oidc",
- "container.oidc",
- "OIDC");
- }
-}
diff --git a/tci-advanced-demo/tci-selenium/README.md b/tci-advanced-demo/tci-selenium/README.md
deleted file mode 100644
index 8fd1a472..00000000
--- a/tci-advanced-demo/tci-selenium/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-This module contains the TCI for Selenium.
-
-Noteworthy contains:
-* multiple Browsers (Firefox + Chrome - all other Browsers are nowadays derivates of them) and a mechanism to handle them correctly.
-* an optional extension that automatically creates videos of e.g. test failures
diff --git a/tci-advanced-demo/tci-selenium/pom.xml b/tci-advanced-demo/tci-selenium/pom.xml
deleted file mode 100644
index 52a27cfa..00000000
--- a/tci-advanced-demo/tci-selenium/pom.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
- 4.0.0
-
- software.xdev.tci.demo
- tci-advanced-demo
- 1.2.1-SNAPSHOT
-
- tci-selenium
-
-
-
- software.xdev.tci.demo
- tci-testcontainers
-
-
-
- software.xdev
- testcontainers-selenium
-
-
-
- org.junit.jupiter
- junit-jupiter-api
- compile
-
-
-
-
- org.seleniumhq.selenium
- selenium-remote-driver
-
-
-
- io.opentelemetry
- *
-
-
-
-
- org.seleniumhq.selenium
- selenium-support
-
-
- org.seleniumhq.selenium
- selenium-firefox-driver
-
-
- org.seleniumhq.selenium
- selenium-chrome-driver
-
-
-
diff --git a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/BrowserTCI.java b/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/BrowserTCI.java
deleted file mode 100644
index d3741086..00000000
--- a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/BrowserTCI.java
+++ /dev/null
@@ -1,178 +0,0 @@
-package software.xdev.tci.demo.tci.selenium;
-
-import static java.util.Collections.emptyMap;
-
-import java.time.Duration;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.openqa.selenium.Capabilities;
-import org.openqa.selenium.remote.HttpCommandExecutor;
-import org.openqa.selenium.remote.RemoteWebDriver;
-import org.openqa.selenium.remote.http.ClientConfig;
-import org.openqa.selenium.remote.http.HttpClient;
-import org.rnorth.ducttape.timeouts.Timeouts;
-import org.rnorth.ducttape.unreliables.Unreliables;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testcontainers.lifecycle.TestDescription;
-
-import software.xdev.tci.TCI;
-import software.xdev.tci.demo.tci.selenium.containers.SeleniumBrowserWebDriverContainer;
-
-
-public class BrowserTCI extends TCI
-{
- private static final Logger LOG = LoggerFactory.getLogger(BrowserTCI.class);
-
- protected Capabilities capabilities;
- protected RemoteWebDriver webDriver;
- protected ClientConfig clientConfig = ClientConfig.defaultConfig();
- protected int webDriverRetryCount = 2;
- protected int webDriverRetrySec = 30;
-
- public BrowserTCI(
- final SeleniumBrowserWebDriverContainer container,
- final String networkAlias,
- final Capabilities capabilities)
- {
- super(container, networkAlias);
- this.capabilities = capabilities;
- }
-
- public BrowserTCI withClientConfig(final ClientConfig clientConfig)
- {
- this.clientConfig = Objects.requireNonNull(clientConfig);
- return this;
- }
-
- public BrowserTCI withWebDriverRetryCount(final int webDriverRetryCount)
- {
- this.webDriverRetryCount = Math.min(Math.max(webDriverRetryCount, 2), 10);
- return this;
- }
-
- public BrowserTCI withWebDriverRetrySec(final int webDriverRetrySec)
- {
- this.webDriverRetrySec = Math.min(Math.max(webDriverRetrySec, 10), 10 * 60);
- return this;
- }
-
- @Override
- public void start(final String containerName)
- {
- super.start(containerName);
-
- this.initWebDriver();
- }
-
- @SuppressWarnings("checkstyle:MagicNumber")
- protected void initWebDriver()
- {
- LOG.debug("Initializing WebDriver");
- final AtomicInteger retryCounter = new AtomicInteger(1);
- this.webDriver = Unreliables.retryUntilSuccess(
- this.webDriverRetryCount,
- () -> {
- final ClientConfig config =
- this.clientConfig.baseUri(this.getContainer().getSeleniumAddressURI());
- final int tryCount = retryCounter.getAndIncrement();
- LOG.info(
- "Creating new WebDriver [retryCount={},retrySec={},clientConfig={}] Try #{}",
- this.webDriverRetryCount,
- this.webDriverRetrySec,
- config,
- tryCount);
-
- final HttpClient.Factory factory = HttpCommandExecutor.getDefaultClientFactory();
- final HttpClient client = factory.createClient(config);
- final HttpCommandExecutor commandExecutor = new HttpCommandExecutor(
- emptyMap(),
- config,
- // Constructor without factory does not exist...
- x -> client);
-
- try
- {
- return Timeouts.getWithTimeout(
- this.webDriverRetrySec,
- TimeUnit.SECONDS,
- () -> new RemoteWebDriver(commandExecutor, this.capabilities));
- }
- catch(final RuntimeException rex)
- {
- // Cancel further communication and abort all connections
- try
- {
- LOG.warn("Encounter problem in try #{} - Terminating communication", tryCount);
- client.close();
- factory.cleanupIdleClients();
- }
- catch(final Exception ex)
- {
- LOG.warn("Failed to cleanup try #{}", tryCount, ex);
- }
-
- throw rex;
- }
- });
-
- // Default timeout is 5m? -> Single test failure causes up to 10m delays (replay must also be saved!)
- // https://w3c.github.io/webdriver/#timeouts
- this.webDriver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(60));
-
- // Maximize window
- this.webDriver.manage().window().maximize();
- }
-
- public Optional getVncAddress()
- {
- return Optional.ofNullable(this.getContainer().getVncAddress());
- }
-
- public Optional getNoVncAddress()
- {
- return Optional.ofNullable(this.getContainer().getNoVncAddress());
- }
-
- public RemoteWebDriver getWebDriver()
- {
- return this.webDriver;
- }
-
- public void afterTest(final TestDescription description, final Optional throwable)
- {
- if(this.getContainer() != null)
- {
- this.getContainer().afterTest(description, throwable);
- }
- }
-
- @Override
- public void stop()
- {
- if(this.webDriver != null)
- {
- final long startTime = System.currentTimeMillis();
- try
- {
- this.webDriver.quit();
- }
- catch(final Exception e)
- {
- LOG.warn("Failed to quit the driver", e);
- }
- finally
- {
- if(LOG.isDebugEnabled())
- {
- LOG.debug("Quiting driver took {}ms", System.currentTimeMillis() - startTime);
- }
- }
- this.webDriver = null;
- }
- super.stop();
- }
-}
diff --git a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/TestBrowser.java b/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/TestBrowser.java
deleted file mode 100644
index 650752ac..00000000
--- a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/TestBrowser.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package software.xdev.tci.demo.tci.selenium;
-
-import java.util.function.Supplier;
-
-import org.openqa.selenium.Capabilities;
-import org.openqa.selenium.chrome.ChromeOptions;
-import org.openqa.selenium.firefox.FirefoxOptions;
-import org.openqa.selenium.firefox.FirefoxProfile;
-
-
-public enum TestBrowser
-{
- FIREFOX(() -> {
- final FirefoxOptions firefoxOptions = new FirefoxOptions();
-
- final FirefoxProfile firefoxProfile = new FirefoxProfile();
- // Allows to type into console without an annoying SELF XSS popup
- firefoxProfile.setPreference("devtools.selfxss.count", "100");
- firefoxOptions.setProfile(firefoxProfile);
-
- return firefoxOptions;
- }),
- CHROME(ChromeOptions::new);
-
- private final Supplier capabilityFactory;
-
- TestBrowser(final Supplier driverFactory)
- {
- this.capabilityFactory = driverFactory;
- }
-
- public Supplier getCapabilityFactory()
- {
- return this.capabilityFactory;
- }
-}
diff --git a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/factory/BrowserTCIFactory.java b/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/factory/BrowserTCIFactory.java
deleted file mode 100644
index 0f8b73ea..00000000
--- a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/factory/BrowserTCIFactory.java
+++ /dev/null
@@ -1,155 +0,0 @@
-package software.xdev.tci.demo.tci.selenium.factory;
-
-import java.nio.file.Path;
-import java.time.Duration;
-import java.util.Optional;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Stream;
-
-import org.openqa.selenium.Capabilities;
-import org.openqa.selenium.remote.http.ClientConfig;
-import org.rnorth.ducttape.unreliables.Unreliables;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
-import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
-import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
-
-import software.xdev.tci.demo.tci.selenium.BrowserTCI;
-import software.xdev.tci.demo.tci.selenium.containers.SeleniumBrowserWebDriverContainer;
-import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
-import software.xdev.tci.misc.ContainerMemory;
-import software.xdev.testcontainers.selenium.containers.browser.BrowserWebDriverContainer;
-import software.xdev.testcontainers.selenium.containers.recorder.SeleniumRecordingContainer;
-
-
-class BrowserTCIFactory extends PreStartableTCIFactory
-{
- private static final Logger LOG = LoggerFactory.getLogger(BrowserTCIFactory.class);
-
- public static final String PROPERTY_RECORD_MODE = "recordMode";
- public static final String PROPERTY_RECORD_DIR = "recordDir";
- public static final String PROPERTY_VNC_ENABLED = "vncEnabled";
-
- public static final String DEFAULT_RECORD_DIR = "target/records";
-
- protected final String browserName;
-
- /*
- * Constants (set by JVM-Property or default value)
- * Only call corresponding methods and don't access the fields directly!
- */
- protected static BrowserWebDriverContainer.RecordingMode systemRecordingMode;
- protected static Path dirForRecords;
- protected static Boolean vncEnabled;
-
- @SuppressWarnings({"resource", "checkstyle:MagicNumber"})
- public BrowserTCIFactory(final Capabilities capabilities)
- {
- super(
- (c, na) -> new BrowserTCI(c, na, capabilities)
- .withClientConfig(ClientConfig.defaultConfig()
- .readTimeout(Duration.ofSeconds(60))),
- () -> new SeleniumBrowserWebDriverContainer(capabilities)
- .withStartRecordingContainerManually(true)
- .withRecordingDirectory(getDefaultDirForRecords())
- .withRecordingMode(getDefaultRecordingMode())
- .withDisableVNC(!isVNCEnabled())
- .withEnableNoVNC(isVNCEnabled())
- .withRecordingContainerSupplier(t -> new SeleniumRecordingContainer(t)
- .withFrameRate(10)
- .withLogConsumer(getLogConsumer("container.browserrecorder." + capabilities.getBrowserName()))
- .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)))
- // Without that a mount volume dialog shows up
- // https://github.com/testcontainers/testcontainers-java/issues/1670
- .withSharedMemorySize(ContainerMemory.M2G)
- .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M1G))
- .withEnv("SE_SCREEN_WIDTH", "1600")
- .withEnv("SE_SCREEN_HEIGHT", "900")
- // By default after 5 mins the session is killed and you can't use the container anymore. Cool or?
- // https://github.com/SeleniumHQ/docker-selenium?tab=readme-ov-file#grid-url-and-session-timeout
- .withEnv("SE_NODE_SESSION_TIMEOUT", "3600")
- // AWS's Raspberry Pi-sized CPUs are completely overloaded with the default 15s timeout so increase it
- .waitingFor(new WaitAllStrategy()
- .withStrategy(new LogMessageWaitStrategy()
- .withRegEx(".*(Started Selenium Standalone).*\n")
- .withStartupTimeout(Duration.ofMinutes(1)))
- .withStrategy(new HostPortWaitStrategy())
- .withStartupTimeout(Duration.ofMinutes(1))),
- "selenium-" + capabilities.getBrowserName().toLowerCase(),
- "container.browserwebdriver." + capabilities.getBrowserName(),
- "Browser-" + capabilities.getBrowserName());
- this.browserName = capabilities.getBrowserName();
- }
-
- @Override
- protected void postProcessNew(final BrowserTCI infra)
- {
- // Start recording container here otherwise there is a lot of blank video
- final CompletableFuture cfStartRecorder =
- CompletableFuture.runAsync(() -> infra.getContainer().startRecordingContainer());
-
- // Docker needs a few milliseconds (usually less than 100) to reconfigure its networks
- // In the meantime existing connections might fail if we go on immediately
- // So let's wait a moment here until everything is fine
- Unreliables.retryUntilSuccess(
- 10,
- TimeUnit.SECONDS,
- () -> infra.getWebDriver().getCurrentUrl());
-
- cfStartRecorder.join();
- }
-
- @Override
- public String getFactoryName()
- {
- return super.getFactoryName() + "-" + this.browserName;
- }
-
- protected static synchronized BrowserWebDriverContainer.RecordingMode getDefaultRecordingMode()
- {
- if(systemRecordingMode != null)
- {
- return systemRecordingMode;
- }
-
- final String propRecordMode = System.getProperty(PROPERTY_RECORD_MODE);
- systemRecordingMode = Stream.of(BrowserWebDriverContainer.RecordingMode.values())
- .filter(rm -> rm.toString().equals(propRecordMode))
- .findFirst()
- .orElse(BrowserWebDriverContainer.RecordingMode.RECORD_FAILING);
- LOG.info("Default Recording Mode='{}'", systemRecordingMode);
-
- return systemRecordingMode;
- }
-
- protected static synchronized Path getDefaultDirForRecords()
- {
- if(dirForRecords != null)
- {
- return dirForRecords;
- }
-
- dirForRecords = Path.of(System.getProperty(PROPERTY_RECORD_DIR, DEFAULT_RECORD_DIR));
- final boolean wasCreated = dirForRecords.toFile().mkdirs();
- LOG.info("Default Directory for records='{}', created={}", dirForRecords.toAbsolutePath(), wasCreated);
-
- return dirForRecords;
- }
-
- protected static synchronized boolean isVNCEnabled()
- {
- if(vncEnabled != null)
- {
- return vncEnabled;
- }
-
- vncEnabled = Optional.ofNullable(System.getProperty(PROPERTY_VNC_ENABLED))
- .map(s -> "1".equals(s) || Boolean.parseBoolean(s))
- .orElse(false);
- LOG.info("VNC enabled={}", vncEnabled);
-
- return vncEnabled;
- }
-}
diff --git a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/testbase/SeleniumIntegrationTestExtension.java b/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/testbase/SeleniumIntegrationTestExtension.java
deleted file mode 100644
index 81c0b573..00000000
--- a/tci-advanced-demo/tci-selenium/src/main/java/software/xdev/tci/demo/tci/selenium/testbase/SeleniumIntegrationTestExtension.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package software.xdev.tci.demo.tci.selenium.testbase;
-
-import java.util.Objects;
-import java.util.function.Function;
-
-import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
-import org.junit.jupiter.api.extension.ExtensionContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.testcontainers.lifecycle.TestDescription;
-
-import software.xdev.tci.demo.tci.selenium.BrowserTCI;
-
-
-/**
- * Extension for Selenium integration tests that creates records for tests if required
- */
-public abstract class SeleniumIntegrationTestExtension implements AfterTestExecutionCallback
-{
- private static final Logger LOG = LoggerFactory.getLogger(SeleniumIntegrationTestExtension.class);
-
- protected final Function tciExtractor;
-
- protected SeleniumIntegrationTestExtension(
- final Function tciExtractor)
- {
- this.tciExtractor = Objects.requireNonNull(tciExtractor);
- }
-
- @Override
- public void afterTestExecution(final ExtensionContext context) throws Exception
- {
- final BrowserTCI browserTCI = this.tciExtractor.apply(context);
- if(browserTCI != null)
- {
- // Wait a moment, so everything is safe on tape
- Thread.sleep(100);
-
- LOG.debug("Trying to capture video");
-
- browserTCI.afterTest(new TestDescription()
- {
- @Override
- public String getTestId()
- {
- return this.getFilesystemFriendlyName();
- }
-
- @SuppressWarnings("checkstyle:MagicNumber")
- @Override
- public String getFilesystemFriendlyName()
- {
- final String testClassName = this.cleanForFilename(context.getRequiredTestClass().getSimpleName());
- final String displayName = this.cleanForFilename(context.getDisplayName());
- return System.currentTimeMillis()
- + "_"
- + testClassName
- + "_"
- // Cut off otherwise file name is too long
- + displayName.substring(0, Math.min(displayName.length(), 200));
- }
-
- private String cleanForFilename(final String str)
- {
- return str.replace(' ', '_')
- .replaceAll("[^A-Za-z0-9#_-]", "")
- .toLowerCase();
- }
- }, context.getExecutionException());
- }
- LOG.debug("AfterTestExecution done");
- }
-}
diff --git a/tci-advanced-demo/tci-testcontainers/README.md b/tci-advanced-demo/tci-testcontainers/README.md
deleted file mode 100644
index 4b65b1c9..00000000
--- a/tci-advanced-demo/tci-testcontainers/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-This module is the base module for all other TCI modules.
-It contains a few shared classes for e.g. correctly redirecting logging.
diff --git a/tci-advanced-demo/tci-testcontainers/pom.xml b/tci-advanced-demo/tci-testcontainers/pom.xml
deleted file mode 100644
index 53f20783..00000000
--- a/tci-advanced-demo/tci-testcontainers/pom.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
- 4.0.0
-
- software.xdev.tci.demo
- tci-advanced-demo
- 1.2.1-SNAPSHOT
-
- tci-testcontainers
-
-
-
- software.xdev
- tci-base
-
-
-
-
- org.slf4j
- jul-to-slf4j
-
-
-
diff --git a/tci-advanced-demo/tci-testcontainers/src/main/java/software/xdev/tci/demo/tci/util/ContainerLoggingUtil.java b/tci-advanced-demo/tci-testcontainers/src/main/java/software/xdev/tci/demo/tci/util/ContainerLoggingUtil.java
deleted file mode 100644
index 4bd9dc34..00000000
--- a/tci-advanced-demo/tci-testcontainers/src/main/java/software/xdev/tci/demo/tci/util/ContainerLoggingUtil.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package software.xdev.tci.demo.tci.util;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.slf4j.bridge.SLF4JBridgeHandler;
-
-
-public class ContainerLoggingUtil
-{
- private static final Logger LOG = LoggerFactory.getLogger(ContainerLoggingUtil.class);
-
- static final ContainerLoggingUtil INSTANCE = new ContainerLoggingUtil();
-
- ContainerLoggingUtil()
- {
- // No impl
- }
-
- public static void redirectJULtoSLF4J()
- {
- INSTANCE.redirectJULtoSLF4JInt();
- }
-
- void redirectJULtoSLF4JInt()
- {
- if(SLF4JBridgeHandler.isInstalled())
- {
- return;
- }
-
- LOG.debug("Installing SLF4JBridgeHandler");
- SLF4JBridgeHandler.removeHandlersForRootLogger();
- SLF4JBridgeHandler.install();
- }
-}