[KEYCLOAK-19274] - Avoid loading queries from properties at runtime for Dist.X

This commit is contained in:
Pedro Igor 2021-09-15 10:19:03 -03:00 committed by Hynek Mlnařík
parent 339224578e
commit 10e425315f
4 changed files with 95 additions and 47 deletions

View file

@ -17,6 +17,10 @@
package org.keycloak.connections.jpa; package org.keycloak.connections.jpa;
import static org.keycloak.connections.jpa.util.JpaUtils.configureNamedQuery;
import static org.keycloak.connections.jpa.util.JpaUtils.getDatabaseType;
import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries;
import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform; import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
@ -25,7 +29,6 @@ import org.keycloak.ServerStartupError;
import org.keycloak.common.util.StackUtil; import org.keycloak.common.util.StackUtil;
import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.migration.MigrationModelManager; import org.keycloak.migration.MigrationModelManager;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -55,8 +58,6 @@ import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import liquibase.Liquibase;
import liquibase.exception.LiquibaseException;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -105,13 +106,16 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
} }
private void addSpecificNamedQueries(KeycloakSession session, Connection connection) { private void addSpecificNamedQueries(KeycloakSession session, Connection connection) {
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
EntityManager em = null; EntityManager em = null;
try { try {
Liquibase liquibase = liquibaseProvider.getLiquibase(connection, this.getSchema());
em = createEntityManager(session); em = createEntityManager(session);
JpaUtils.addSpecificNamedQueries(em, liquibase.getDatabase().getShortName()); String dbKind = getDatabaseType(connection.getMetaData().getDatabaseProductName());
} catch (LiquibaseException e) { for (Map.Entry<Object, Object> query : loadSpecificNamedQueries(dbKind.toLowerCase()).entrySet()) {
String queryName = query.getKey().toString();
String querySql = query.getValue().toString();
configureNamedQuery(queryName, querySql, em);
}
} catch (SQLException e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);
} finally { } finally {
JpaUtils.closeEntityManager(em); JpaUtils.closeEntityManager(em);

View file

@ -17,9 +17,9 @@
package org.keycloak.connections.jpa.util; package org.keycloak.connections.jpa.util;
import org.jboss.logging.Logger;
import org.hibernate.engine.query.spi.sql.NativeSQLQueryReturn; import org.hibernate.engine.query.spi.sql.NativeSQLQueryReturn;
import org.hibernate.engine.query.spi.sql.NativeSQLQuerySpecification; import org.hibernate.engine.query.spi.sql.NativeSQLQuerySpecification;
import org.jboss.logging.Logger;
import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
import org.hibernate.jpa.boot.internal.PersistenceXmlParser; import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
@ -110,7 +110,7 @@ public class JpaUtils {
* @param url The url to load, it can be null * @param url The url to load, it can be null
* @return A properties file with the url loaded or null * @return A properties file with the url loaded or null
*/ */
private static Properties loadSqlProperties(URL url) { public static Properties loadSqlProperties(URL url) {
if (url == null) { if (url == null) {
return null; return null;
} }
@ -174,41 +174,71 @@ public class JpaUtils {
* that database type. * that database type.
* @param em The entity manager to use * @param em The entity manager to use
* @param databaseType The database type as managed in * @param databaseType The database type as managed in
* <a href="https://www.liquibase.org/get-started/databases">liquibase</a>. * @return
*/ */
public static void addSpecificNamedQueries(EntityManager em, String databaseType) { public static Properties loadSpecificNamedQueries(String databaseType) {
final SessionFactoryImplementor sfi = em.getEntityManagerFactory().unwrap(SessionFactoryImplementor.class);
URL specificUrl = JpaUtils.class.getClassLoader().getResource("META-INF/queries-" + databaseType + ".properties"); URL specificUrl = JpaUtils.class.getClassLoader().getResource("META-INF/queries-" + databaseType + ".properties");
URL defaultUrl = JpaUtils.class.getClassLoader().getResource("META-INF/queries-default.properties"); URL defaultUrl = JpaUtils.class.getClassLoader().getResource("META-INF/queries-default.properties");
if (defaultUrl == null) { if (defaultUrl == null) {
throw new IllegalStateException("META-INF/queries-default.properties was not found in the classpath"); throw new IllegalStateException("META-INF/queries-default.properties was not found in the classpath");
} }
Properties specificQueries = loadSqlProperties(specificUrl); Properties specificQueries = loadSqlProperties(specificUrl);
Properties defaultQueries = loadSqlProperties(defaultUrl); Properties defaultQueries = loadSqlProperties(defaultUrl);
Properties queries = new Properties();
for (String queryNameFull : defaultQueries.stringPropertyNames()) { for (String queryNameFull : defaultQueries.stringPropertyNames()) {
String querySql; String querySql = defaultQueries.getProperty(queryNameFull);
String queryName = getQueryShortName(queryNameFull); String queryName = getQueryShortName(queryNameFull);
String specificQueryNameFull = getQueryFromProperties(queryName, specificQueries); String specificQueryNameFull = getQueryFromProperties(queryName, specificQueries);
if (specificQueryNameFull != null) { if (specificQueryNameFull != null) {
// the query is redefined in the specific database file => use it // the query is redefined in the specific database file => use it
queryNameFull = specificQueryNameFull; queryNameFull = specificQueryNameFull;
querySql = specificQueries.getProperty(queryNameFull); querySql = specificQueries.getProperty(queryNameFull);
} else {
// use the default query sql
querySql = defaultQueries.getProperty(queryNameFull);
} }
boolean isNative = queryNameFull.endsWith(QUERY_NATIVE_SUFFIX);
logger.tracef("adding query from properties files native=%b %s:%s", isNative, queryName, querySql); queries.put(queryNameFull, querySql);
if (isNative) { }
NativeSQLQuerySpecification spec = new NativeSQLQuerySpecification(querySql, new NativeSQLQueryReturn[0], Collections.emptySet());
sfi.getQueryPlanCache().getNativeSQLQueryPlan(spec); return queries;
em.getEntityManagerFactory().addNamedQuery(queryName, em.createNativeQuery(querySql)); }
} else {
sfi.getQueryPlanCache().getHQLQueryPlan(querySql, false, Collections.emptyMap()); /**
em.getEntityManagerFactory().addNamedQuery(queryName, em.createQuery(querySql)); * Configures a named query to Hibernate.
} *
* @param queryName the query name
* @param querySql the query SQL
* @param entityManager the entity manager
*/
public static void configureNamedQuery(String queryName, String querySql, EntityManager entityManager) {
boolean isNative = queryName.endsWith(QUERY_NATIVE_SUFFIX);
queryName = getQueryShortName(queryName);
logger.tracef("adding query from properties files native=%b %s:%s", isNative, queryName, querySql);
SessionFactoryImplementor sessionFactory = entityManager.getEntityManagerFactory().unwrap(SessionFactoryImplementor.class);
if (isNative) {
NativeSQLQuerySpecification spec = new NativeSQLQuerySpecification(querySql, new NativeSQLQueryReturn[0], Collections.emptySet());
sessionFactory.getQueryPlanCache().getNativeSQLQueryPlan(spec);
sessionFactory.addNamedQuery(queryName, entityManager.createNativeQuery(querySql));
} else {
sessionFactory.getQueryPlanCache().getHQLQueryPlan(querySql, false, Collections.emptyMap());
sessionFactory.addNamedQuery(queryName, entityManager.createQuery(querySql));
}
}
public static String getDatabaseType(String productName) {
switch (productName) {
case "Microsoft SQL Server":
case "SQLOLEDB":
return "mssql";
case "EnterpriseDB":
return "postgresql";
default:
return productName.toLowerCase();
} }
} }

View file

@ -20,6 +20,8 @@ package org.keycloak.quarkus.deployment;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
import static org.keycloak.configuration.Configuration.getPropertyNames; import static org.keycloak.configuration.Configuration.getPropertyNames;
import static org.keycloak.configuration.Configuration.getRawValue; import static org.keycloak.configuration.Configuration.getRawValue;
import static org.keycloak.connections.jpa.QuarkusJpaConnectionProviderFactory.QUERY_PROPERTY_PREFIX;
import static org.keycloak.connections.jpa.util.JpaUtils.loadSpecificNamedQueries;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS; import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS; import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES; import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
@ -38,15 +40,15 @@ import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException; import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import java.util.Properties; import java.util.Properties;
import java.util.ServiceLoader;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
import io.quarkus.deployment.builditem.IndexDependencyBuildItem; import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
@ -72,13 +74,14 @@ import org.keycloak.authorization.policy.provider.PolicySpi;
import org.keycloak.authorization.policy.provider.js.DeployedScriptPolicyFactory; import org.keycloak.authorization.policy.provider.js.DeployedScriptPolicyFactory;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.common.util.StreamUtil; import org.keycloak.common.util.StreamUtil;
import org.keycloak.config.ConfigProviderFactory;
import org.keycloak.configuration.Configuration; import org.keycloak.configuration.Configuration;
import org.keycloak.configuration.KeycloakConfigSourceProvider; import org.keycloak.configuration.KeycloakConfigSourceProvider;
import org.keycloak.configuration.MicroProfileConfigProvider; import org.keycloak.configuration.MicroProfileConfigProvider;
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory; import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.QuarkusJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory; import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider; import org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.protocol.ProtocolMapperSpi; import org.keycloak.protocol.ProtocolMapperSpi;
import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper; import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper;
import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.provider.EnvironmentDependentProviderFactory;
@ -155,12 +158,20 @@ class KeycloakProcessor {
*/ */
@Record(ExecutionTime.STATIC_INIT) @Record(ExecutionTime.STATIC_INIT)
@BuildStep @BuildStep
void configureHibernate(KeycloakRecorder recorder, HibernateOrmConfig config, List<PersistenceUnitDescriptorBuildItem> descriptors) { void configureHibernate(KeycloakRecorder recorder, HibernateOrmConfig config, List<PersistenceUnitDescriptorBuildItem> descriptors,
List<JdbcDataSourceBuildItem> jdbcDataSources) {
PersistenceUnitDescriptor unit = descriptors.get(0).asOutputPersistenceUnitDefinition(emptyList()).getActualHibernateDescriptor(); PersistenceUnitDescriptor unit = descriptors.get(0).asOutputPersistenceUnitDefinition(emptyList()).getActualHibernateDescriptor();
Properties unitProperties = unit.getProperties();
unit.getProperties().setProperty(AvailableSettings.DIALECT, config.defaultPersistenceUnit.dialect.dialect.orElse(null)); unitProperties.setProperty(AvailableSettings.DIALECT, config.defaultPersistenceUnit.dialect.dialect.orElse(null));
unit.getProperties().setProperty(AvailableSettings.JPA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name()); unitProperties.setProperty(AvailableSettings.JPA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name());
unit.getProperties().setProperty(AvailableSettings.QUERY_STARTUP_CHECKING, Boolean.FALSE.toString()); unitProperties.setProperty(AvailableSettings.QUERY_STARTUP_CHECKING, Boolean.FALSE.toString());
String dbKind = jdbcDataSources.get(0).getDbKind();
for (Entry<Object, Object> query : loadSpecificNamedQueries(dbKind.toLowerCase()).entrySet()) {
unitProperties.setProperty(QUERY_PROPERTY_PREFIX + query.getKey(), query.getValue().toString());
}
} }
/** /**
@ -179,11 +190,11 @@ class KeycloakProcessor {
Map<Class<? extends Provider>, String> defaultProviders = new HashMap<>(); Map<Class<? extends Provider>, String> defaultProviders = new HashMap<>();
Map<String, ProviderFactory> preConfiguredProviders = new HashMap<>(); Map<String, ProviderFactory> preConfiguredProviders = new HashMap<>();
for (Map.Entry<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> entry : loadFactories(preConfiguredProviders) for (Entry<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> entry : loadFactories(preConfiguredProviders)
.entrySet()) { .entrySet()) {
checkProviders(entry.getKey(), entry.getValue(), defaultProviders); checkProviders(entry.getKey(), entry.getValue(), defaultProviders);
for (Map.Entry<Class<? extends Provider>, Map<String, ProviderFactory>> value : entry.getValue().entrySet()) { for (Entry<Class<? extends Provider>, Map<String, ProviderFactory>> value : entry.getValue().entrySet()) {
for (ProviderFactory factory : value.getValue().values()) { for (ProviderFactory factory : value.getValue().values()) {
factories.computeIfAbsent(entry.getKey(), factories.computeIfAbsent(entry.getKey(),
key -> new HashMap<>()) key -> new HashMap<>())
@ -393,7 +404,7 @@ class KeycloakProcessor {
descriptor = JsonSerialization.readValue(is, ScriptProviderDescriptor.class); descriptor = JsonSerialization.readValue(is, ScriptProviderDescriptor.class);
} }
for (Map.Entry<String, List<ScriptProviderMetadata>> entry : descriptor.getProviders().entrySet()) { for (Entry<String, List<ScriptProviderMetadata>> entry : descriptor.getProviders().entrySet()) {
if (isScriptForSpi(spi, entry.getKey())) { if (isScriptForSpi(spi, entry.getKey())) {
for (ScriptProviderMetadata metadata : entry.getValue()) { for (ScriptProviderMetadata metadata : entry.getValue()) {
ProviderFactory provider = createDeployableScriptProvider(jarFile, entry, metadata); ProviderFactory provider = createDeployableScriptProvider(jarFile, entry, metadata);
@ -411,7 +422,7 @@ class KeycloakProcessor {
return providers; return providers;
} }
private ProviderFactory createDeployableScriptProvider(JarFile jarFile, Map.Entry<String, List<ScriptProviderMetadata>> entry, private ProviderFactory createDeployableScriptProvider(JarFile jarFile, Entry<String, List<ScriptProviderMetadata>> entry,
ScriptProviderMetadata metadata) throws IOException { ScriptProviderMetadata metadata) throws IOException {
String fileName = metadata.getFileName(); String fileName = metadata.getFileName();

View file

@ -17,6 +17,7 @@
package org.keycloak.connections.jpa; package org.keycloak.connections.jpa;
import static org.keycloak.connections.jpa.util.JpaUtils.configureNamedQuery;
import static org.keycloak.connections.liquibase.QuarkusJpaUpdaterProvider.VERIFY_AND_RUN_MASTER_CHANGELOG; import static org.keycloak.connections.liquibase.QuarkusJpaUpdaterProvider.VERIFY_AND_RUN_MASTER_CHANGELOG;
import static org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction; import static org.keycloak.models.utils.KeycloakModelUtils.runJobInTransaction;
@ -28,6 +29,7 @@ import java.sql.DatabaseMetaData;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -43,8 +45,8 @@ import javax.transaction.Transaction;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import io.quarkus.runtime.Quarkus; import io.quarkus.runtime.Quarkus;
import liquibase.Liquibase;
import liquibase.exception.LiquibaseException; import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.SessionFactoryImpl; import org.hibernate.internal.SessionFactoryImpl;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
@ -52,7 +54,6 @@ import org.keycloak.ServerStartupError;
import org.keycloak.common.Version; import org.keycloak.common.Version;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.exportimport.ExportImportConfig; import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.exportimport.ExportImportManager; import org.keycloak.exportimport.ExportImportManager;
import org.keycloak.migration.MigrationModelManager; import org.keycloak.migration.MigrationModelManager;
@ -80,6 +81,7 @@ import org.keycloak.util.JsonSerialization;
*/ */
public final class QuarkusJpaConnectionProviderFactory implements JpaConnectionProviderFactory, ServerInfoAwareProviderFactory { public final class QuarkusJpaConnectionProviderFactory implements JpaConnectionProviderFactory, ServerInfoAwareProviderFactory {
public static final String QUERY_PROPERTY_PREFIX = "kc.query.";
private static final Logger logger = Logger.getLogger(QuarkusJpaConnectionProviderFactory.class); private static final Logger logger = Logger.getLogger(QuarkusJpaConnectionProviderFactory.class);
private static final String SQL_GET_LATEST_VERSION = "SELECT VERSION FROM %sMIGRATION_MODEL"; private static final String SQL_GET_LATEST_VERSION = "SELECT VERSION FROM %sMIGRATION_MODEL";
@ -116,14 +118,15 @@ public final class QuarkusJpaConnectionProviderFactory implements JpaConnectionP
} }
private void addSpecificNamedQueries(KeycloakSession session, Connection connection) { private void addSpecificNamedQueries(KeycloakSession session, Connection connection) {
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class); SessionFactoryImplementor sfi = emf.unwrap(SessionFactoryImplementor.class);
EntityManager em = null; EntityManager em = createEntityManager(session);
try { try {
Liquibase liquibase = liquibaseProvider.getLiquibase(connection, this.getSchema()); Map<String, Object> unitProperties = emf.getProperties();
em = createEntityManager(session);
JpaUtils.addSpecificNamedQueries(em, liquibase.getDatabase().getShortName()); unitProperties.entrySet().stream()
} catch (LiquibaseException e) { .filter(entry -> entry.getKey().startsWith(QUERY_PROPERTY_PREFIX))
throw new IllegalStateException(e); .forEach(entry -> configureNamedQuery(entry.getKey().substring(QUERY_PROPERTY_PREFIX.length()), entry.getValue().toString(), em));
} finally { } finally {
JpaUtils.closeEntityManager(em); JpaUtils.closeEntityManager(em);
} }