diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/Maven.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/Maven.java
index 2f707a58632..ee902e57461 100644
--- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/Maven.java
+++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/Maven.java
@@ -139,12 +139,7 @@ public final class Maven {
public static Path getKeycloakQuarkusModulePath() {
// Find keycloak-parent module first
- BootstrapMavenContext ctx = null;
- try {
- ctx = bootstrapCurrentMavenContext();
- } catch (BootstrapMavenException | URISyntaxException e) {
- throw new RuntimeException("Failed bootstrap maven context", e);
- }
+ BootstrapMavenContext ctx = bootstrapCurrentMavenContext();
for (LocalProject m = ctx.getCurrentProject(); m != null; m = m.getLocalParent()) {
if ("keycloak-parent".equals(m.getArtifactId())) {
// When found, advance to quarkus module
@@ -155,14 +150,18 @@ public final class Maven {
throw new RuntimeException("Failed to find keycloak-parent module.");
}
- public static BootstrapMavenContext bootstrapCurrentMavenContext() throws BootstrapMavenException, URISyntaxException {
- if (context == null) {
- Path classPathDir = Paths.get(Thread.currentThread().getContextClassLoader().getResource(".").toURI());
- Path projectDir = BuildToolHelper.getProjectDir(classPathDir);
- context = new BootstrapMavenContext(
- BootstrapMavenContext.config().setPreferPomsFromWorkspace(true).setWorkspaceModuleParentHierarchy(true)
- .setCurrentProject(projectDir.toString()));
+ public static BootstrapMavenContext bootstrapCurrentMavenContext() {
+ try {
+ if (context == null) {
+ Path classPathDir = Paths.get(Thread.currentThread().getContextClassLoader().getResource(".").toURI());
+ Path projectDir = BuildToolHelper.getProjectDir(classPathDir);
+ context = new BootstrapMavenContext(
+ BootstrapMavenContext.config().setPreferPomsFromWorkspace(true).setWorkspaceModuleParentHierarchy(true)
+ .setCurrentProject(projectDir.toString()));
+ }
+ return context;
+ } catch (BootstrapMavenException | URISyntaxException e) {
+ throw new RuntimeException("Failed bootstrap maven context", e);
}
- return context;
}
}
diff --git a/test-framework/core/pom.xml b/test-framework/core/pom.xml
index b0def975613..bf6a7adb907 100755
--- a/test-framework/core/pom.xml
+++ b/test-framework/core/pom.xml
@@ -81,6 +81,14 @@
io.quarkus
quarkus-bootstrap-maven-resolver
+
+ org.jboss.shrinkwrap
+ shrinkwrap-api
+
+
+ org.jboss.shrinkwrap
+ shrinkwrap-impl-base
+
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/AuthenticationFlowConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/AuthenticationFlowConfigBuilder.java
index 9a148e7d74a..d76b1577c00 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/AuthenticationFlowConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/AuthenticationFlowConfigBuilder.java
@@ -2,6 +2,7 @@ package org.keycloak.testframework.realm;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+import org.keycloak.testframework.util.Collections;
public class AuthenticationFlowConfigBuilder {
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientConfigBuilder.java
index 72408cd2d8d..7bae14f6bf5 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/ClientConfigBuilder.java
@@ -6,6 +6,7 @@ import java.util.List;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.testframework.util.Collections;
public class ClientConfigBuilder {
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/GroupConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/GroupConfigBuilder.java
index 3277f80a1a5..faaaa3a70e7 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/GroupConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/GroupConfigBuilder.java
@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.testframework.util.Collections;
public class GroupConfigBuilder {
private final GroupRepresentation rep;
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java
index 3a9a25c8784..11b5f0fac35 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RealmConfigBuilder.java
@@ -18,6 +18,7 @@ import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testframework.util.Collections;
public class RealmConfigBuilder {
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RoleConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RoleConfigBuilder.java
index 348bad94e9c..52c78781336 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/RoleConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/RoleConfigBuilder.java
@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Map;
import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.testframework.util.Collections;
public class RoleConfigBuilder {
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java
index 29774502079..2c1bfe733b0 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java
@@ -10,6 +10,7 @@ import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testframework.util.Collections;
public class UserConfigBuilder {
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java
index ba3bbf1e8cb..17ff0249a11 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/DistributionKeycloakServer.java
@@ -8,20 +8,15 @@ import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
-import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
-import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import java.util.stream.Collectors;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
@@ -39,7 +34,6 @@ import org.keycloak.testframework.util.ProcessUtils;
import org.keycloak.testframework.util.TmpDir;
import io.quarkus.fs.util.ZipUtils;
-import io.quarkus.maven.dependency.Dependency;
import org.jboss.logging.Logger;
import org.jetbrains.annotations.NotNull;
@@ -69,7 +63,6 @@ public class DistributionKeycloakServer implements KeycloakServer {
this.tlsEnabled = tlsEnabled;
List args = keycloakServerConfigBuilder.toArgs();
- Set dependencies = keycloakServerConfigBuilder.toDependencies();
try {
boolean installationCreated = createInstallation();
@@ -77,8 +70,7 @@ public class DistributionKeycloakServer implements KeycloakServer {
killPreviousProcess();
}
- File providersDir = new File(keycloakHomeDir, "providers");
- List existingProviders = listExistingProviders(providersDir);
+ ProviderDeployer providerDeployer = new ProviderDeployer(log, keycloakHomeDir, keycloakServerConfigBuilder.toDependencies(), KeycloakServer.getDependencyHotDeployEnabled());
if (!installationCreated && reuse && ping()) {
checkRunning();
@@ -87,10 +79,8 @@ public class DistributionKeycloakServer implements KeycloakServer {
String startedWithArgs = startupArgsFile.isFile() ? FileUtils.readStringFromFile(startupArgsFile) : null;
String requestedArgs = String.join(" ", args);
- Set requestedDependencies = dependencies.stream().map(d -> d.getGroupId() + "__" + d.getArtifactId() + ".jar").collect(Collectors.toSet());
- Set startedWithDependencies = existingProviders.stream().map(File::getName).collect(Collectors.toSet());
-
- if (requestedArgs.equals(startedWithArgs) && setEquals(requestedDependencies, startedWithDependencies)) {
+ boolean dependenciesChanged = providerDeployer.updateDependencies();
+ if (requestedArgs.equals(startedWithArgs) && !dependenciesChanged) {
log.trace("Re-using already running Keycloak");
return;
} else {
@@ -100,10 +90,10 @@ public class DistributionKeycloakServer implements KeycloakServer {
throw new RuntimeException("Running Keycloak not started with required arguments or providers, and could not kill the current process");
}
}
+ } else {
+ providerDeployer.updateDependencies();
}
- updateProviders(existingProviders, dependencies, providersDir);
-
OutputHandler outputHandler = startKeycloak(args);
waitForStart(outputHandler);
@@ -174,37 +164,6 @@ public class DistributionKeycloakServer implements KeycloakServer {
return outputHandler;
}
- private static void updateProviders(List existingProviders, Set dependencies, File providersDir) throws IOException {
- existingProviders.stream()
- .filter(f -> f.getName().endsWith(".jar"))
- .filter(f -> {
- String fileName = f.getName();
- String groupId = fileName.substring(0, fileName.indexOf("__"));
- String artifactId = fileName.substring(fileName.indexOf("__") + 2, fileName.lastIndexOf(".jar"));
- return dependencies.stream().noneMatch(d -> d.getGroupId().equals(groupId) && d.getArtifactId().equals(artifactId));
- }).forEach(f -> {
- log.trace("Deleted non-requested provider: " + f.getAbsolutePath());
- FileUtils.delete(f);
- FileUtils.delete(new File(f.getAbsolutePath() + ".lastModified"));
- });
-
- Path providersPath = providersDir.toPath();
- for (Dependency d : dependencies) {
- Path dependencyPath = Maven.resolveArtifact(d.getGroupId(), d.getArtifactId());
- File dependencyFile = dependencyPath.toFile();
- Path targetPath = providersPath.resolve(d.getGroupId() + "__" + d.getArtifactId() + ".jar");
- File targetFile = targetPath.toFile();
- File targetLastModified = new File(targetFile.getAbsolutePath() + ".lastModified");
- long lastModified = targetLastModified.isFile() ? FileUtils.readLongFromFile(targetLastModified) : -1;
-
- if (lastModified != dependencyPath.toFile().lastModified() || !targetFile.isFile()) {
- log.trace("Adding or overriding existing provider: " + targetPath.toFile().getAbsolutePath());
- Files.copy(dependencyPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
- Files.writeString(targetLastModified.toPath(), Long.toString(dependencyFile.lastModified()));
- }
- }
- }
-
@Override
public void stop() {
if (!reuse) {
@@ -370,20 +329,6 @@ public class DistributionKeycloakServer implements KeycloakServer {
return Maven.resolveArtifact("org.keycloak", "keycloak-quarkus-dist").toFile();
}
- private boolean setEquals(Set a, Set b) {
- return a.size() == b.size() && a.containsAll(b);
- }
-
- private List listExistingProviders(File providersDir) {
- if (providersDir.isDirectory()) {
- File[] files = providersDir.listFiles(n -> n.getName().endsWith(".jar"));
- if (files != null) {
- return Arrays.stream(files).toList();
- }
- }
- return List.of();
- }
-
private class OutputHandler implements Runnable {
private static final Pattern LOG_PATTERN = Pattern.compile("([^ ]*) ([^ ]*) ([A-Z]*)([ ]*)(.*)");
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java
index 62998ad7c28..030e06e8ed6 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/EmbeddedKeycloakServer.java
@@ -7,7 +7,6 @@ import org.keycloak.Keycloak;
import org.keycloak.common.Version;
import org.keycloak.it.utils.Maven;
-import io.quarkus.maven.dependency.Dependency;
import org.eclipse.aether.artifact.Artifact;
public class EmbeddedKeycloakServer implements KeycloakServer {
@@ -20,7 +19,7 @@ public class EmbeddedKeycloakServer implements KeycloakServer {
Keycloak.Builder builder = Keycloak.builder().setVersion(Version.VERSION);
this.tlsEnabled = tlsEnabled;
- for(Dependency dependency : keycloakServerConfigBuilder.toDependencies()) {
+ for(KeycloakDependency dependency : keycloakServerConfigBuilder.toDependencies()) {
var version = Optional.ofNullable(Maven.getArtifact(dependency.getGroupId(), dependency.getArtifactId()))
.map(Artifact::getVersion)
.orElse("");
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakDependency.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakDependency.java
new file mode 100644
index 00000000000..9eed2fddba2
--- /dev/null
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakDependency.java
@@ -0,0 +1,58 @@
+package org.keycloak.testframework.server;
+
+import io.quarkus.maven.dependency.ArtifactDependency;
+import io.quarkus.maven.dependency.DependencyBuilder;
+
+public class KeycloakDependency extends ArtifactDependency {
+
+ private final boolean hotDeployable;
+ private final boolean dependencyCurrentProject;
+
+ private KeycloakDependency(Builder dependencyBuilder) {
+ super(dependencyBuilder);
+ this.hotDeployable = dependencyBuilder.hotDeployable;
+ this.dependencyCurrentProject = dependencyBuilder.dependencyCurrentProject;
+ }
+
+ public boolean isHotDeployable() {
+ return this.hotDeployable;
+ }
+
+ public boolean dependencyCurrentProject() {
+ return this.dependencyCurrentProject;
+ }
+
+ public static class Builder extends DependencyBuilder {
+
+ private boolean hotDeployable = false;
+ private boolean dependencyCurrentProject = false;
+
+ public Builder hotDeployable(boolean hotDeployable) {
+ this.hotDeployable = hotDeployable;
+ return this;
+ }
+
+ public Builder dependencyCurrentProject(boolean dependencyCurrentProject) {
+ this.dependencyCurrentProject = dependencyCurrentProject;
+ return this;
+ }
+
+ @Override
+ public Builder setGroupId(String groupId) {
+ super.setGroupId(groupId);
+ return this;
+ }
+
+ @Override
+ public Builder setArtifactId(String artifactId) {
+ super.setArtifactId(artifactId);
+ return this;
+ }
+
+ @Override
+ public KeycloakDependency build() {
+ return new KeycloakDependency(this);
+ }
+
+ }
+}
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServer.java
index c68d9270a43..d38c30e7186 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServer.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServer.java
@@ -1,5 +1,7 @@
package org.keycloak.testframework.server;
+import org.keycloak.testframework.config.Config;
+
public interface KeycloakServer {
void start(KeycloakServerConfigBuilder keycloakServerConfigBuilder, boolean tlsEnabled);
@@ -10,4 +12,8 @@ public interface KeycloakServer {
String getManagementBaseUrl();
+ static boolean getDependencyHotDeployEnabled() {
+ return Boolean.parseBoolean(Config.getValueTypeConfig(KeycloakServer.class, "hot.deploy", "false", String.class));
+ }
+
}
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java
index d22ad46964d..d0c2322d8bb 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java
@@ -12,8 +12,6 @@ import java.util.stream.Collectors;
import org.keycloak.common.Profile;
import org.keycloak.testframework.infinispan.CacheType;
-import io.quarkus.maven.dependency.Dependency;
-import io.quarkus.maven.dependency.DependencyBuilder;
import io.smallrye.config.SmallRyeConfig;
import org.eclipse.microprofile.config.spi.ConfigSource;
@@ -26,7 +24,7 @@ public class KeycloakServerConfigBuilder {
private final Set features = new HashSet<>();
private final Set featuresDisabled = new HashSet<>();
private final LogBuilder log = new LogBuilder();
- private final Set dependencies = new HashSet<>();
+ private final Set dependencies = new HashSet<>();
private CacheType cacheType = CacheType.LOCAL;
private boolean externalInfinispan = false;
@@ -172,10 +170,29 @@ public class KeycloakServerConfigBuilder {
* @return
*/
public KeycloakServerConfigBuilder dependency(String groupId, String artifactId) {
- dependencies.add(new DependencyBuilder().setGroupId(groupId).setArtifactId(artifactId).build());
+ return dependency(groupId, artifactId, false, false);
+ }
+
+ public KeycloakServerConfigBuilder dependency(String groupId, String artifactId, boolean hotDeployable) {
+ return dependency(groupId, artifactId, hotDeployable, false);
+ }
+
+ private KeycloakServerConfigBuilder dependency(String groupId, String artifactId, boolean hotDeployable, boolean dependencyCurrentProject) {
+ dependencies.add(
+ new KeycloakDependency.Builder()
+ .setGroupId(groupId)
+ .setArtifactId(artifactId)
+ .hotDeployable(hotDeployable)
+ .dependencyCurrentProject(dependencyCurrentProject)
+ .build()
+ );
return this;
}
+ public KeycloakServerConfigBuilder dependencyCurrentProject() {
+ return dependency("", "", false, true);
+ }
+
public class LogBuilder {
private Boolean color;
@@ -288,7 +305,7 @@ public class KeycloakServerConfigBuilder {
return args;
}
- Set toDependencies() {
+ Set toDependencies() {
return dependencies;
}
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/ProviderDeployer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/ProviderDeployer.java
new file mode 100644
index 00000000000..7957174bf85
--- /dev/null
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/ProviderDeployer.java
@@ -0,0 +1,141 @@
+package org.keycloak.testframework.server;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.keycloak.it.utils.Maven;
+import org.keycloak.testframework.util.FileUtils;
+import org.keycloak.testframework.util.MavenProjectUtil;
+
+import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject;
+import org.jboss.logging.Logger;
+
+final class ProviderDeployer {
+
+ private final Logger log;
+ private final File providersDir;
+ private final boolean hotDeployEnabled;
+ private final Set requestedDependencies;
+
+ ProviderDeployer(Logger log, File keycloakHomeDir, Set requestedDependencies, boolean hotDeployEnabled) {
+ this.log = log;
+ this.providersDir = new File(keycloakHomeDir, "providers");
+ this.requestedDependencies = requestedDependencies;
+ this.hotDeployEnabled = hotDeployEnabled;
+ }
+
+ boolean updateDependencies() throws IOException {
+ boolean anyDependenciesModified = deleteNotRequestedDependencies();
+
+ for (KeycloakDependency d : requestedDependencies) {
+ boolean shouldPackageClasses = hotDeployEnabled && d.isHotDeployable();
+
+ String jarName = getDependencyJarName(d);
+
+ Path dependencyPath = getDependencyPath(d);
+ Path targetPath = providersDir.toPath().resolve(jarName);
+
+ File targetFile = targetPath.toFile();
+
+ long dependencyLastModified = getMostRecentModification(dependencyPath);
+ File targetLastModifiedFile = new File(targetFile.getAbsolutePath() + ".lastModified");
+ long targetLastModified = targetLastModifiedFile.isFile() ? FileUtils.readLongFromFile(targetLastModifiedFile) : -1;
+
+ if (dependencyLastModified != targetLastModified || !targetFile.isFile()) {
+ log.trace("Adding or overwriting existing provider: " + targetPath.toFile().getAbsolutePath());
+
+ if (shouldPackageClasses || d.dependencyCurrentProject()) {
+ MavenProjectUtil.buildJar(jarName, dependencyPath, targetPath);
+ } else {
+ Files.copy(dependencyPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
+ }
+ Files.writeString(targetLastModifiedFile.toPath(), Long.toString(dependencyLastModified));
+ anyDependenciesModified = true;
+ }
+ }
+ return anyDependenciesModified;
+ }
+
+ private String getDependencyJarName(KeycloakDependency dependency) {
+ String groupId = dependency.getGroupId();
+ String artifactId = dependency.getArtifactId();
+
+ if (dependency.dependencyCurrentProject()) {
+ LocalProject project = MavenProjectUtil.getCurrentModule();
+
+ groupId = project.getGroupId();
+ artifactId = project.getArtifactId();
+ }
+
+ return groupId + "__" + artifactId + ".jar";
+ }
+
+ private boolean deleteNotRequestedDependencies() {
+ Set requestedJarNames = requestedDependencies.stream()
+ .map(this::getDependencyJarName)
+ .collect(Collectors.toSet());
+
+ List toDelete = listExistingDependencies().stream()
+ .filter(f -> !requestedJarNames.contains(f.getName()))
+ .toList();
+
+ for (File f : toDelete) {
+ String path = f.getAbsolutePath();
+ log.trace("Deleted non-requested provider: " + path);
+ FileUtils.delete(f);
+ FileUtils.delete(new File(path + ".lastModified"));
+ }
+
+ return !toDelete.isEmpty();
+ }
+
+ private List listExistingDependencies() {
+ if (providersDir.isDirectory()) {
+ File[] files = providersDir.listFiles(n -> n.getName().endsWith(".jar"));
+ if (files != null) {
+ return Arrays.stream(files).toList();
+ }
+ }
+ return List.of();
+ }
+
+ private Path getDependencyPath(KeycloakDependency d) {
+ if (d.dependencyCurrentProject()) {
+ return MavenProjectUtil.getCurrentModule().getClassesDir();
+ }
+
+ if (d.isHotDeployable() && hotDeployEnabled) {
+ return MavenProjectUtil.findLocalModule(d.getGroupId(), d.getArtifactId()).getClassesDir();
+ }
+
+ return Maven.resolveArtifact(d.getGroupId(), d.getArtifactId());
+ }
+
+ private long getMostRecentModification(Path path) throws IOException {
+ File file = path.toFile();
+ if (!file.exists()) {
+ return 0;
+ }
+
+ if (file.isFile()) {
+ return file.lastModified();
+ }
+
+ try (Stream stream = Files.walk(path)) {
+ return stream
+ .filter(Files::isRegularFile)
+ .mapToLong(p -> p.toFile().lastModified())
+ .max()
+ .orElse(0);
+ }
+ }
+
+}
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java
index 882833630ec..3643beef6d6 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java
@@ -10,8 +10,6 @@ import javax.net.ssl.SSLException;
import org.keycloak.it.utils.Maven;
import org.keycloak.testframework.config.Config;
-import io.quarkus.maven.dependency.Dependency;
-
import static java.lang.System.out;
public class RemoteKeycloakServer implements KeycloakServer {
@@ -62,10 +60,10 @@ public class RemoteKeycloakServer implements KeycloakServer {
out.println(String.join(" \\\n", config.toArgs()));
out.println();
- Set dependencies = config.toDependencies();
+ Set dependencies = config.toDependencies();
if (!dependencies.isEmpty()) {
out.println("Requested providers:");
- for (Dependency d : dependencies) {
+ for (KeycloakDependency d : dependencies) {
out.println("* " + d.getGroupId() + ":" + d.getArtifactId());
}
out.println();
@@ -76,7 +74,7 @@ public class RemoteKeycloakServer implements KeycloakServer {
out.println("Remote Keycloak server is not running on " + getBaseUrl() + ", please start Keycloak with:");
out.println();
- Set dependencies = config.toDependencies();
+ Set dependencies = config.toDependencies();
if (!dependencies.isEmpty()) {
String dependencyPaths = dependencies.stream().map(d -> Maven.resolveArtifact(d.getGroupId(), d.getArtifactId()).toString()).collect(Collectors.joining(","));
out.println("KCW_PROVIDERS=" + dependencyPaths + " \\");
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/realm/Collections.java b/test-framework/core/src/main/java/org/keycloak/testframework/util/Collections.java
similarity index 63%
rename from test-framework/core/src/main/java/org/keycloak/testframework/realm/Collections.java
rename to test-framework/core/src/main/java/org/keycloak/testframework/util/Collections.java
index 0d9e04a062b..726d03bc44d 100644
--- a/test-framework/core/src/main/java/org/keycloak/testframework/realm/Collections.java
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/util/Collections.java
@@ -1,4 +1,4 @@
-package org.keycloak.testframework.realm;
+package org.keycloak.testframework.util;
import java.util.Arrays;
import java.util.HashMap;
@@ -10,12 +10,12 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-class Collections {
+public class Collections {
private Collections() {
}
- static List combine(List l1, List l2) {
+ public static List combine(List l1, List l2) {
if (l1 == null) {
return new LinkedList<>(l2);
} else {
@@ -25,16 +25,16 @@ class Collections {
}
@SafeVarargs
- static List combine(List l1, T... items) {
+ public static List combine(List l1, T... items) {
return combine(l1, Arrays.asList(items));
}
- static List combine(List l1, Stream items) {
+ public static List combine(List l1, Stream items) {
return combine(l1, items.toList());
}
- static Set combine(Set s1, Set s2) {
+ public static Set combine(Set s1, Set s2) {
if (s1 == null) {
return new HashSet<>(s2);
} else {
@@ -44,16 +44,16 @@ class Collections {
}
@SafeVarargs
- static Set combine(Set s1, T... items) {
+ public static Set combine(Set s1, T... items) {
return combine(s1, Set.of(items));
}
- static Set combine(Set s1, Stream items) {
+ public static Set combine(Set s1, Stream items) {
return combine(s1, items.collect(Collectors.toSet()));
}
- static Map> combine(Map> m1, Map> m2) {
+ public static Map> combine(Map> m1, Map> m2) {
if (m1 == null) {
m1 = new HashMap<>();
}
@@ -66,11 +66,11 @@ class Collections {
}
@SafeVarargs
- static Map> combine(Map> m1, K key, V... values) {
+ public static Map> combine(Map> m1, K key, V... values) {
return combine(m1, Map.of(key, List.of(values)));
}
- static Map> combine(Map> m1, K key, Stream values) {
+ public static Map> combine(Map> m1, K key, Stream values) {
return combine(m1, Map.of(key, values.toList()));
}
diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/util/MavenProjectUtil.java b/test-framework/core/src/main/java/org/keycloak/testframework/util/MavenProjectUtil.java
new file mode 100644
index 00000000000..577e4dcbd1e
--- /dev/null
+++ b/test-framework/core/src/main/java/org/keycloak/testframework/util/MavenProjectUtil.java
@@ -0,0 +1,80 @@
+package org.keycloak.testframework.util;
+
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+import org.keycloak.it.utils.Maven;
+
+import io.quarkus.bootstrap.resolver.maven.BootstrapMavenContext;
+import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject;
+import org.jboss.shrinkwrap.api.ShrinkWrap;
+import org.jboss.shrinkwrap.api.exporter.ZipExporter;
+import org.jboss.shrinkwrap.api.spec.JavaArchive;
+
+
+public final class MavenProjectUtil {
+
+ private static LocalProject rootModuleProject;
+
+ private static LocalProject getRootModule() {
+ if (rootModuleProject != null) {
+ return rootModuleProject;
+ }
+
+ BootstrapMavenContext ctx = Maven.bootstrapCurrentMavenContext();
+ LocalProject m = ctx.getCurrentProject();
+ while (m.getLocalParent() != null) {
+ m = m.getLocalParent();
+ }
+ rootModuleProject = m;
+ return rootModuleProject;
+ }
+
+ public static LocalProject findLocalModule(String groupId, String artifactId) {
+ LocalProject rootModule = getRootModule();
+ LocalProject dependencyModule = rootModule.getWorkspace().getProject(groupId, artifactId);
+ if (dependencyModule == null) {
+ throw new RuntimeException("Failed to resolve artifact in this project: [" + groupId + ":" + artifactId + "]");
+ }
+ return dependencyModule;
+ }
+
+ public static LocalProject getCurrentModule() {
+ BootstrapMavenContext ctx = Maven.bootstrapCurrentMavenContext();
+ return ctx.getCurrentProject();
+ }
+
+ /**
+ * Builds and exports a JAR from compiled classes and resources.
+ *
+ * @param jarName the JAR filename
+ * @param classesPath path to compiled output directory ({@code target/classes})
+ * @param targetPath path where to export the JAR
+ */
+ public static void buildJar(String jarName, Path classesPath, Path targetPath) {
+ JavaArchive providerJar = ShrinkWrap.create(JavaArchive.class, jarName);
+
+ try (Stream sourcePathStream = Files.walk(classesPath)) {
+ sourcePathStream.filter(Files::isRegularFile)
+ .forEach(p -> {
+ String relativeFilePath = classesPath.relativize(p).toString();
+
+ if (relativeFilePath.endsWith(".class")) {
+ String fullyQualifiedClassName = relativeFilePath.replace(File.separatorChar, '.').substring(0, relativeFilePath.lastIndexOf('.'));
+ providerJar.addClass(fullyQualifiedClassName);
+ } else {
+ File resourceFile = p.toFile();
+ providerJar.addAsResource(resourceFile, relativeFilePath);
+ }
+ });
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ providerJar.as(ZipExporter.class).exportTo(targetPath.toFile(), true);
+ }
+}
diff --git a/test-framework/examples/providers/pom.xml b/test-framework/examples/providers/pom.xml
index fb8bd77ab74..d2766f53695 100644
--- a/test-framework/examples/providers/pom.xml
+++ b/test-framework/examples/providers/pom.xml
@@ -36,6 +36,12 @@
jakarta.ws.rs
jakarta.ws.rs-api
+
+ org.keycloak.testframework
+ keycloak-test-framework-core
+ ${project.version}
+ test
+
diff --git a/test-framework/examples/providers/src/main/java/org/keycloak/providers/example/MyCustomRealmResourceProvider.java b/test-framework/examples/providers/src/main/java/org/keycloak/providers/example/MyCustomRealmResourceProvider.java
index 6dd9431d31e..64aeb738e53 100644
--- a/test-framework/examples/providers/src/main/java/org/keycloak/providers/example/MyCustomRealmResourceProvider.java
+++ b/test-framework/examples/providers/src/main/java/org/keycloak/providers/example/MyCustomRealmResourceProvider.java
@@ -11,6 +11,8 @@ import org.keycloak.services.resource.RealmResourceProvider;
/**
*
+ * @see org.keycloak.providers.example.MyCustomProviderWithinSameModuleTest
+ * @see org.keycloak.test.examples.MyCustomProviderTest
* @author Simon Vacek
*/
public class MyCustomRealmResourceProvider implements RealmResourceProvider {
diff --git a/test-framework/examples/providers/src/test/java/org/keycloak/providers/example/MyCustomProviderWithinSameModuleTest.java b/test-framework/examples/providers/src/test/java/org/keycloak/providers/example/MyCustomProviderWithinSameModuleTest.java
new file mode 100644
index 00000000000..55eaddd1b18
--- /dev/null
+++ b/test-framework/examples/providers/src/test/java/org/keycloak/providers/example/MyCustomProviderWithinSameModuleTest.java
@@ -0,0 +1,51 @@
+package org.keycloak.providers.example;
+
+import java.io.IOException;
+import java.net.URL;
+
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.http.simple.SimpleHttp;
+import org.keycloak.testframework.annotations.InjectRealm;
+import org.keycloak.testframework.annotations.InjectSimpleHttp;
+import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
+import org.keycloak.testframework.realm.ManagedRealm;
+import org.keycloak.testframework.server.KeycloakServerConfig;
+import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+
+/**
+ *
+ * @see org.keycloak.providers.example.MyCustomRealmResourceProvider
+ * @see org.keycloak.test.examples.MyCustomProviderTest
+ * @author Simon Vacek
+ */
+@KeycloakIntegrationTest(config = MyCustomProviderWithinSameModuleTest.ServerConfig.class)
+public class MyCustomProviderWithinSameModuleTest {
+
+ @InjectRealm
+ ManagedRealm realm;
+
+ @InjectSimpleHttp
+ SimpleHttp simpleHttp;
+
+ @Test
+ public void httpGetTest() throws IOException {
+ URL url = KeycloakUriBuilder.fromUri(realm.getBaseUrl()).path("/custom-provider/hello").build().toURL();
+
+ String response = simpleHttp.doGet(url.toString()).header("Accept", "text/plain").asString();
+
+ Assertions.assertEquals("Hello World!", response);
+ }
+
+ public static class ServerConfig implements KeycloakServerConfig {
+
+ @Override
+ public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
+ return config.dependencyCurrentProject();
+ }
+
+ }
+}
diff --git a/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/MyCustomProviderTest.java b/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/MyCustomProviderTest.java
index 81ab619e1ef..94c9a26ed95 100644
--- a/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/MyCustomProviderTest.java
+++ b/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/MyCustomProviderTest.java
@@ -1,25 +1,25 @@
package org.keycloak.test.examples;
import java.io.IOException;
-import java.nio.charset.StandardCharsets;
+import java.net.URL;
+import org.keycloak.common.util.KeycloakUriBuilder;
+import org.keycloak.http.simple.SimpleHttp;
import org.keycloak.testframework.annotations.InjectRealm;
+import org.keycloak.testframework.annotations.InjectSimpleHttp;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
-import org.apache.commons.io.IOUtils;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
*
+ * @see org.keycloak.providers.example.MyCustomRealmResourceProvider
+ * @see org.keycloak.providers.example.MyCustomProviderWithinSameModuleTest
* @author Simon Vacek
*/
@KeycloakIntegrationTest(config = MyCustomProviderTest.ServerConfig.class)
@@ -28,25 +28,23 @@ public class MyCustomProviderTest {
@InjectRealm
ManagedRealm realm;
+ @InjectSimpleHttp
+ SimpleHttp simpleHttp;
+
@Test
- public void httpGetTest() {
- String url = realm.getBaseUrl();
+ public void httpGetTest() throws IOException {
+ URL url = KeycloakUriBuilder.fromUri(realm.getBaseUrl()).path("/custom-provider/hello").build().toURL();
- HttpUriRequest request = new HttpGet(url + "/custom-provider/hello");
- try {
- HttpResponse response = HttpClientBuilder.create().build().execute(request);
- Assertions.assertEquals(200, response.getStatusLine().getStatusCode());
+ String response = simpleHttp.doGet(url.toString()).header("Accept", "text/plain").asString();
- String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
- Assertions.assertEquals("Hello World!", content);
- } catch (IOException ignored) {}
+ Assertions.assertEquals("Hello World!", response);
}
public static class ServerConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
- return config.dependency("org.keycloak.testframework", "keycloak-test-framework-example-providers");
+ return config.dependency("org.keycloak.testframework", "keycloak-test-framework-example-providers", true);
}
}
diff --git a/test-framework/pom.xml b/test-framework/pom.xml
index fdceea37806..f98017a42c0 100755
--- a/test-framework/pom.xml
+++ b/test-framework/pom.xml
@@ -34,6 +34,7 @@
2.0.3
+ 1.2.6
@@ -79,6 +80,16 @@
testcontainers-postgresql
${version.testcontainers}
+
+ org.jboss.shrinkwrap
+ shrinkwrap-api
+ ${version.shrinkwrap}
+
+
+ org.jboss.shrinkwrap
+ shrinkwrap-impl-base
+ ${version.shrinkwrap}
+