diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index f91a2072d1b..0441d9c89c0 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -53,6 +53,8 @@ import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.RouteBuildItem; import jakarta.persistence.Entity; import jakarta.persistence.PersistenceUnitTransactionType; +import org.eclipse.microprofile.config.spi.ConfigSource; +import org.hibernate.cfg.JdbcSettings; import org.eclipse.microprofile.health.Readiness; import org.hibernate.cfg.AvailableSettings; import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; @@ -154,6 +156,7 @@ import java.util.Optional; import java.util.Properties; import java.util.ServiceLoader; import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.jar.JarEntry; @@ -366,6 +369,47 @@ class KeycloakProcessor { recorder.setDefaultUserProfileConfiguration(configuration.getDefaultConfig()); } + /** + * Get datasource name obtained from the persistence.xml file based on this order: + *
    + *
  1. return {@link JdbcSettings#JAKARTA_JTA_DATASOURCE} if specified + *
  2. return {@link AvailableSettings#DATASOURCE} property if specified + *
  3. return persistence unit name + *
+ * Can be removed after removing support for persistence.xml files + */ + static String getDatasourceNameFromPersistenceXml(PersistenceUnitDescriptor descriptor) { + if (descriptor == null) { + throw new IllegalStateException("Descriptor cannot be null"); + } + final BiConsumer infoAboutUsedSourceForDsName = (source, name) -> logger.debugf( + "Datasource name '%s' is obtained from the '%s' configuration property in persistence.xml file. " + + "Use '%s' name for datasource options like 'db-kind-%s'.", name, source, name, name); + + String persistenceUnitName = descriptor.getName(); + Properties properties = descriptor.getProperties(); + + // 1. return Jakarta properties + var jakartaProperty = properties.getProperty(JdbcSettings.JAKARTA_JTA_DATASOURCE); + if (jakartaProperty != null) { + infoAboutUsedSourceForDsName.accept(JdbcSettings.JAKARTA_JTA_DATASOURCE, jakartaProperty); + return jakartaProperty; + } + + // 2. return deprecated Hibernate property + var deprecatedHibernateProperty = properties.getProperty(AvailableSettings.DATASOURCE); + if (deprecatedHibernateProperty != null) { + logger.warnf("Property '%s' is deprecated for some time and you should rather use '%s' property for datasource name in persistence.xml file.", + AvailableSettings.DATASOURCE, JdbcSettings.JAKARTA_JTA_DATASOURCE); + infoAboutUsedSourceForDsName.accept(AvailableSettings.DATASOURCE, deprecatedHibernateProperty); + return deprecatedHibernateProperty; + } + + // 3. return persistence unit name + infoAboutUsedSourceForDsName.accept("Persistence unit name", persistenceUnitName); + return persistenceUnitName; + } + /** *

Configures the persistence unit for Quarkus. * @@ -398,10 +442,11 @@ class KeycloakProcessor { runtimeConfigured.produce(new HibernateOrmIntegrationRuntimeConfiguredBuildItem("keycloak", defaultUnitDescriptor.getName()) .setInitListener(recorder.createDefaultUnitListener())); } else { - Properties properties = descriptor.getProperties(); + String datasourceName = getDatasourceNameFromPersistenceXml(descriptor); + configurePersistenceUnitProperties(datasourceName, descriptor); // register a listener for customizing the unit configuration at runtime runtimeConfigured.produce(new HibernateOrmIntegrationRuntimeConfiguredBuildItem("keycloak", descriptor.getName()) - .setInitListener(recorder.createUserDefinedUnitListener(properties.getProperty(AvailableSettings.DATASOURCE)))); + .setInitListener(recorder.createUserDefinedUnitListener(datasourceName))); userManagedEntities.addAll(descriptor.getManagedClassNames()); } } @@ -427,6 +472,25 @@ class KeycloakProcessor { producer.produce(new PersistenceXmlDescriptorBuildItem(descriptor)); } + static void configurePersistenceUnitProperties(String datasourceName, ParsedPersistenceXmlDescriptor descriptor) { + Properties unitProperties = descriptor.getProperties(); + var isResourceLocalSpecified = PersistenceUnitTransactionType.RESOURCE_LOCAL.equals(descriptor.getPersistenceUnitTransactionType()) || + Optional.ofNullable(unitProperties.getProperty(AvailableSettings.JAKARTA_TRANSACTION_TYPE)) + .map(f -> f.equalsIgnoreCase(PersistenceUnitTransactionType.RESOURCE_LOCAL.name())) + .orElse(false); + if (isResourceLocalSpecified) { + throw new IllegalArgumentException("You need to use '%s' transaction type in your persistence.xml file." + .formatted(PersistenceUnitTransactionType.JTA.name())); + } + + unitProperties.setProperty(AvailableSettings.JAKARTA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name()); + descriptor.setTransactionType(PersistenceUnitTransactionType.JTA); + + // set datasource name + unitProperties.setProperty(JdbcSettings.JAKARTA_JTA_DATASOURCE,datasourceName); + unitProperties.setProperty(AvailableSettings.DATASOURCE, datasourceName); // for backward compatibility + } + private void configureDefaultPersistenceUnitProperties(ParsedPersistenceXmlDescriptor descriptor, HibernateOrmConfig config, JdbcDataSourceBuildItem defaultDataSource) { if (defaultDataSource == null || !defaultDataSource.isDefault()) { @@ -566,7 +630,7 @@ class KeycloakProcessor { } /** - * Register the custom {@link org.eclipse.microprofile.config.spi.ConfigSource} implementations. + * Register the custom {@link ConfigSource} implementations. * * @param configSources */ diff --git a/quarkus/deployment/src/test/java/org/keycloak/quarkus/deployment/PersistenceXmlDatasourcesTest.java b/quarkus/deployment/src/test/java/org/keycloak/quarkus/deployment/PersistenceXmlDatasourcesTest.java new file mode 100644 index 00000000000..86a812ef142 --- /dev/null +++ b/quarkus/deployment/src/test/java/org/keycloak/quarkus/deployment/PersistenceXmlDatasourcesTest.java @@ -0,0 +1,200 @@ +package org.keycloak.quarkus.deployment; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.JdbcSettings; +import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; +import org.hibernate.jpa.boot.spi.PersistenceXmlParser; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.function.Consumer; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.keycloak.quarkus.deployment.KeycloakProcessor.configurePersistenceUnitProperties; +import static org.keycloak.quarkus.deployment.KeycloakProcessor.getDatasourceNameFromPersistenceXml; +import static org.wildfly.common.Assert.assertNotNull; + +public class PersistenceXmlDatasourcesTest { + private static final String PERSISTENCE_XML_BODY = """ + + + %s + + + """; + + private static PersistenceXmlParser parser; + + @BeforeAll + public static void setupParser() { + parser = PersistenceXmlParser.create(); + } + + @Test + public void datasourceNamesOrder() throws IOException { + // use Jakarta property + var content = """ + + + + + + """; + assertUsedName(content, "user-store"); + + // use Hibernate property + content = """ + + + + + + """; + assertUsedName(content, "my-store"); + + // use persistence unit name + content = """ + + + """; + assertUsedName(content, "user-store-pu"); + + // prefer Jakarta property + content = """ + + + + + + + """; + assertUsedName(content, "user-store"); + + // prefer Hibernate property as not accepting nonJta datasource + content = """ + + + + + + + """; + assertUsedName(content, "my-store"); + } + + @Test + public void transactionTypes() throws IOException { + // not specified transaction-type -> error + var content = """ + + + + + + """; + assertPersistenceXmlSingleDS(content, descriptor -> { + var exception = assertThrows(IllegalArgumentException.class, () -> configurePersistenceUnitProperties("user-store", descriptor)); + assertThat(exception.getMessage(), is("You need to use 'JTA' transaction type in your persistence.xml file.")); + }); + + // jta data source is specified, so the tx type is JTA by default -> ok + content = """ + + JDBC/something + + + + + """; + assertPersistenceXmlSingleDS(content, descriptor -> { + assertDoesNotThrow(() -> configurePersistenceUnitProperties("user-store", descriptor)); + }); + + // tx type is set to RESOURCE_LOCAL -> error + content = """ + + + + + + """; + assertPersistenceXmlSingleDS(content, descriptor -> { + var exception = assertThrows(IllegalArgumentException.class, () -> configurePersistenceUnitProperties("user-store", descriptor)); + assertThat(exception.getMessage(), is("You need to use 'JTA' transaction type in your persistence.xml file.")); + }); + + // Jakarta TX prop is set to RESOURCE_LOCAL -> error + content = """ + + JDBC/something + + + + + + """; + assertPersistenceXmlSingleDS(content, descriptor -> { + var exception = assertThrows(IllegalArgumentException.class, () -> configurePersistenceUnitProperties("user-store", descriptor)); + assertThat(exception.getMessage(), is("You need to use 'JTA' transaction type in your persistence.xml file.")); + }); + + // Everything is correct, we can check if the Jakarta prop is automatically set -> ok + content = """ + + + + + + """; + assertPersistenceXmlSingleDS(content, descriptor -> { + configurePersistenceUnitProperties("user-store", descriptor); + assertThat(descriptor.getProperties().getProperty(AvailableSettings.JAKARTA_TRANSACTION_TYPE), is("JTA")); + }); + } + + private void assertUsedName(String content, String expectedName) throws IOException { + assertPersistenceXmlSingleDS(content, descriptor -> { + var name = getDatasourceNameFromPersistenceXml(descriptor); + assertThat(name, is(expectedName)); + configurePersistenceUnitProperties(name, descriptor); + var properties = descriptor.getProperties(); + assertNotNull(properties); + assertThat(properties.getProperty(JdbcSettings.JAKARTA_JTA_DATASOURCE), is(expectedName)); + assertThat(properties.getProperty(AvailableSettings.DATASOURCE), is(expectedName)); + }); + } + + private void assertPersistenceXmlSingleDS(String content, Consumer asserts) throws IOException { + assertPersistenceXml(content, descriptors -> { + assertNotNull(descriptors); + assertThat(descriptors.size(), is(1)); + var descriptor = descriptors.get(0); + assertNotNull(descriptor); + asserts.accept(descriptor); + }); + } + + private void assertPersistenceXml(String content, Consumer> asserts) throws IOException { + String finalPersistenceXmlFileContent = PERSISTENCE_XML_BODY.formatted(content); + Path persistenceXmlFile = null; + try { + persistenceXmlFile = Files.createTempFile("persistence", ".xml"); + Files.writeString(persistenceXmlFile, finalPersistenceXmlFileContent); + asserts.accept(parser.parse(List.of(persistenceXmlFile.toUri().toURL())).values().stream().map(f -> (ParsedPersistenceXmlDescriptor) f).toList()); + } finally { + if (persistenceXmlFile != null) { + Files.deleteIfExists(persistenceXmlFile); + } + } + } +} diff --git a/quarkus/tests/integration/src/test-providers/resources/com/acme/provider/legacy/jpa/entity/persistence.xml b/quarkus/tests/integration/src/test-providers/resources/com/acme/provider/legacy/jpa/entity/persistence.xml index d4ad48e88cb..418210967cd 100644 --- a/quarkus/tests/integration/src/test-providers/resources/com/acme/provider/legacy/jpa/entity/persistence.xml +++ b/quarkus/tests/integration/src/test-providers/resources/com/acme/provider/legacy/jpa/entity/persistence.xml @@ -25,8 +25,7 @@ - - + diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CustomJpaEntityProviderDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CustomJpaEntityProviderDistTest.java index 65fa7f21d81..56f9acdb60a 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CustomJpaEntityProviderDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/CustomJpaEntityProviderDistTest.java @@ -34,8 +34,10 @@ public class CustomJpaEntityProviderDistTest { @Test @TestProvider(CustomJpaEntityProvider.class) - @Launch({ "start-dev", "--log-level=org.hibernate.jpa.internal.util.LogHelper:debug" }) + @Launch({ "start-dev", "--log-level=org.hibernate.jpa.internal.util.LogHelper:debug,org.keycloak.quarkus.deployment.KeycloakProcessor:debug" }) void testUserManagedEntityNotAddedToDefaultPU(CLIResult cliResult) { + cliResult.assertMessage("Multiple datasources are specified: , user-store"); + cliResult.assertMessage("Datasource name 'user-store' is obtained from the 'jakarta.persistence.jtaDataSource' configuration property in persistence.xml file. Use 'user-store' name for datasource options like 'db-kind-user-store'."); cliResult.assertStringCount("name: user-store", 1); cliResult.assertStringCount("com.acme.provider.legacy.jpa.entity.Realm", 1); cliResult.assertStartedDevMode();